This commit is contained in:
SamGondelman 2018-02-15 15:50:36 -08:00
commit b189433855
75 changed files with 4842 additions and 3166 deletions

View file

@ -464,32 +464,41 @@ void AssetServer::handleAssetMappingOperation(QSharedPointer<ReceivedMessage> me
auto replyPacket = NLPacketList::create(PacketType::AssetMappingOperationReply, QByteArray(), true, true);
replyPacket->writePrimitive(messageID);
bool canWriteToAssetServer = true;
if (senderNode) {
canWriteToAssetServer = senderNode->getCanWriteToAssetServer();
}
switch (operationType) {
case AssetMappingOperationType::Get:
handleGetMappingOperation(*message, senderNode, *replyPacket);
handleGetMappingOperation(*message, *replyPacket);
break;
case AssetMappingOperationType::GetAll:
handleGetAllMappingOperation(*message, senderNode, *replyPacket);
handleGetAllMappingOperation(*replyPacket);
break;
case AssetMappingOperationType::Set:
handleSetMappingOperation(*message, senderNode, *replyPacket);
handleSetMappingOperation(*message, canWriteToAssetServer, *replyPacket);
break;
case AssetMappingOperationType::Delete:
handleDeleteMappingsOperation(*message, senderNode, *replyPacket);
handleDeleteMappingsOperation(*message, canWriteToAssetServer, *replyPacket);
break;
case AssetMappingOperationType::Rename:
handleRenameMappingOperation(*message, senderNode, *replyPacket);
handleRenameMappingOperation(*message, canWriteToAssetServer, *replyPacket);
break;
case AssetMappingOperationType::SetBakingEnabled:
handleSetBakingEnabledOperation(*message, senderNode, *replyPacket);
handleSetBakingEnabledOperation(*message, canWriteToAssetServer, *replyPacket);
break;
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacketList(std::move(replyPacket), *senderNode);
if (senderNode) {
nodeList->sendPacketList(std::move(replyPacket), *senderNode);
} else {
nodeList->sendPacketList(std::move(replyPacket), message->getSenderSockAddr());
}
}
void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketList& replyPacket) {
QString assetPath = message.readString();
QUrl url { assetPath };
@ -568,7 +577,7 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode
}
}
void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
void AssetServer::handleGetAllMappingOperation(NLPacketList& replyPacket) {
replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError);
uint32_t count = (uint32_t)_fileMappings.size();
@ -591,8 +600,8 @@ void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedN
}
}
void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanWriteToAssetServer()) {
void AssetServer::handleSetMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket) {
if (hasWriteAccess) {
QString assetPath = message.readString();
auto assetHash = message.read(AssetUtils::SHA256_HASH_LENGTH).toHex();
@ -614,8 +623,8 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode
}
}
void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanWriteToAssetServer()) {
void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket) {
if (hasWriteAccess) {
int numberOfDeletedMappings { 0 };
message.readPrimitive(&numberOfDeletedMappings);
@ -642,8 +651,8 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared
}
}
void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanWriteToAssetServer()) {
void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket) {
if (hasWriteAccess) {
QString oldPath = message.readString();
QString newPath = message.readString();
@ -664,8 +673,8 @@ void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedN
}
}
void AssetServer::handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanWriteToAssetServer()) {
void AssetServer::handleSetBakingEnabledOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket) {
if (hasWriteAccess) {
bool enabled { true };
message.readPrimitive(&enabled);
@ -739,9 +748,14 @@ void AssetServer::handleAssetGet(QSharedPointer<ReceivedMessage> message, Shared
}
void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
bool canWriteToAssetServer = true;
if (senderNode) {
canWriteToAssetServer = senderNode->getCanWriteToAssetServer();
}
if (senderNode->getCanWriteToAssetServer()) {
qCDebug(asset_server) << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
if (canWriteToAssetServer) {
qCDebug(asset_server) << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(message->getSourceID());
auto task = new UploadAssetTask(message, senderNode, _filesDirectory, _filesizeLimit);
_transferTaskPool.start(task);
@ -761,7 +775,11 @@ void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, Sha
// send off the packet
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacket(std::move(permissionErrorPacket), *senderNode);
if (senderNode) {
nodeList->sendPacket(std::move(permissionErrorPacket), *senderNode);
} else {
nodeList->sendPacket(std::move(permissionErrorPacket), message->getSenderSockAddr());
}
}
}

View file

@ -24,9 +24,6 @@
#include "RegisteredMetaTypes.h"
struct AssetMeta {
AssetMeta() {
}
int bakeVersion { 0 };
bool failedLastBake { false };
QString lastBakeErrors;
@ -55,14 +52,15 @@ private slots:
void sendStatsPacket() override;
private:
using Mappings = std::unordered_map<QString, QString>;
void handleGetMappingOperation(ReceivedMessage& message, NLPacketList& replyPacket);
void handleGetAllMappingOperation(NLPacketList& replyPacket);
void handleSetMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket);
void handleDeleteMappingsOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket);
void handleRenameMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket);
void handleSetBakingEnabledOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket);
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);
void handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleAssetServerBackup(ReceivedMessage& message, NLPacketList& replyPacket);
void handleAssetServerRestore(ReceivedMessage& message, NLPacketList& replyPacket);
// Mapping file operations must be called from main assignment thread only
bool loadMappingsFromFile();
@ -106,7 +104,7 @@ private:
/// Remove baked paths when the original asset is deleteds
void removeBakedPathsForDeletedAsset(AssetUtils::AssetHash originalAssetHash);
Mappings _fileMappings;
AssetUtils::Mappings _fileMappings;
QDir _resourcesDirectory;
QDir _filesDirectory;

View file

@ -112,5 +112,9 @@ void SendAssetTask::run() {
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacketList(std::move(replyPacketList), *_senderNode);
if (_senderNode) {
nodeList->sendPacketList(std::move(replyPacketList), *_senderNode);
} else {
nodeList->sendPacketList(std::move(replyPacketList), _message->getSenderSockAddr());
}
}

View file

@ -41,9 +41,12 @@ void UploadAssetTask::run() {
uint64_t fileSize;
buffer.read(reinterpret_cast<char*>(&fileSize), sizeof(fileSize));
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from"
<< uuidStringWithoutCurlyBraces(_senderNode->getUUID());
if (_senderNode) {
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID());
} else {
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from" << _receivedMessage->getSenderSockAddr();
}
auto replyPacket = NLPacket::create(PacketType::AssetUploadReply, -1, true);
replyPacket->writePrimitive(messageID);
@ -55,9 +58,12 @@ void UploadAssetTask::run() {
auto hash = AssetUtils::hashData(fileData);
auto hexHash = hash.toHex();
qDebug() << "Hash for uploaded file from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID())
<< "is: (" << hexHash << ") ";
if (_senderNode) {
qDebug() << "Hash for uploaded file from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID()) << "is: (" << hexHash << ")";
} else {
qDebug() << "Hash for uploaded file from" << _receivedMessage->getSenderSockAddr() << "is: (" << hexHash << ")";
}
QFile file { _resourcesDir.filePath(QString(hexHash)) };
@ -103,5 +109,9 @@ void UploadAssetTask::run() {
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacket(std::move(replyPacket), *_senderNode);
if (_senderNode) {
nodeList->sendPacket(std::move(replyPacket), *_senderNode);
} else {
nodeList->sendPacket(std::move(replyPacket), _receivedMessage->getSenderSockAddr());
}
}

View file

@ -1,12 +1,6 @@
{
"version": 2.1,
"settings": [
{
"name": "label",
"label": "Label",
"settings": [
]
},
{
"name": "metaverse",
"label": "Metaverse / Networking",
@ -15,7 +9,8 @@
"name": "access_token",
"label": "Access Token",
"help": "This is your OAuth access token to connect this domain-server with your High Fidelity account. <br/>It can be generated by clicking the 'Connect Account' button above.<br/>You can also go to the <a href='https://metaverse.highfidelity.com/user/security' target='_blank'>My Security</a> page of your account and generate a token with the 'domains' scope and paste it here.",
"advanced": true
"advanced": true,
"backup": false
},
{
"name": "id",
@ -55,8 +50,8 @@
]
},
{
"label": "Places / Paths",
"html_id": "places_paths",
"label": "Paths",
"html_id": "paths",
"restart": false,
"settings": [
{
@ -64,6 +59,7 @@
"label": "Paths",
"help": "Clients can enter a path to reach an exact viewpoint in your domain.<br/>Add rows to the table below to map a path to a viewpoint.<br/>The index path ( / ) is where clients will enter if they do not enter an explicit path.",
"type": "table",
"content_setting": true,
"can_add_new_rows": true,
"key": {
"name": "path",
@ -164,7 +160,8 @@
{
"name": "http_username",
"label": "HTTP Username",
"help": "Username used for basic HTTP authentication."
"help": "Username used for basic HTTP authentication.",
"backup": false
},
{
"name": "http_password",
@ -172,7 +169,8 @@
"type": "password",
"help": "Password used for basic HTTP authentication. Leave this alone if you do not want to change it.",
"password_placeholder": "******",
"value-hidden": true
"value-hidden": true,
"backup": false
},
{
"name": "verify_http_password",
@ -935,6 +933,7 @@
"name": "persistent_scripts",
"type": "table",
"label": "Persistent Scripts",
"content_setting": true,
"help": "Add the URLs for scripts that you would like to ensure are always running in your domain.",
"can_add_new_rows": true,
"columns": [
@ -955,99 +954,6 @@
}
]
},
{
"name": "asset_server",
"label": "Asset Server (ATP)",
"assignment-types": [ 3 ],
"settings": [
{
"name": "enabled",
"type": "checkbox",
"label": "Enabled",
"help": "Assigns an asset-server in your domain to serve files to clients via the ATP protocol (over UDP)",
"default": true,
"advanced": true
},
{
"name": "assets_path",
"type": "string",
"label": "Assets Path",
"help": "The path to the directory assets are stored in.<br/>If this path is relative, it will be relative to the application data directory.<br/>If you change this path you will need to manually copy any existing assets from the previous directory.",
"default": "",
"advanced": true
},
{
"name": "assets_filesize_limit",
"type": "int",
"label": "File Size Limit",
"help": "The file size limit of an asset that can be imported into the asset server in MBytes. 0 (default) means no limit on file size.",
"default": 0,
"advanced": true
}
]
},
{
"name": "entity_script_server",
"label": "Entity Script Server (ESS)",
"assignment-types": [ 5 ],
"settings": [
{
"name": "entity_pps_per_script",
"label": "Entity PPS per script",
"help": "The number of packets per second (PPS) that can be sent to the entity server for each server entity script. This contributes to a total overall amount.<br/>Example: 1000 PPS with 5 entites gives a total PPS of 5000 that is shared among the entity scripts. A single could use 4000 PPS, leaving 1000 for the rest, for example.",
"default": 900,
"type": "int",
"advanced": true
},
{
"name": "max_total_entity_pps",
"label": "Maximum Total Entity PPS",
"help": "The maximum total packets per seconds (PPS) that can be sent to the entity server.<br/>Example: 5 scripts @ 1000 PPS per script = 5000 total PPS. A maximum total PPS of 4000 would cap this 5000 total PPS to 4000.",
"default": 9000,
"type": "int",
"advanced": true
}
]
},
{
"name": "avatars",
"label": "Avatars",
"assignment-types": [ 1, 2 ],
"settings": [
{
"name": "min_avatar_height",
"type": "double",
"label": "Minimum Avatar Height (meters)",
"help": "Limits the height of avatars in your domain. Must be at least 0.009.",
"placeholder": 0.4,
"default": 0.4
},
{
"name": "max_avatar_height",
"type": "double",
"label": "Maximum Avatar Height (meters)",
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1755.",
"placeholder": 5.2,
"default": 5.2
},
{
"name": "avatar_whitelist",
"label": "Avatars Allowed from:",
"help": "Comma separated list of URLs (with optional paths) that avatar .fst files are allowed from. If someone attempts to use an avatar with a different domain, it will be rejected and the replacement avatar will be used. If left blank, any domain is allowed.",
"placeholder": "",
"default": "",
"advanced": true
},
{
"name": "replacement_avatar",
"label": "Replacement Avatar for disallowed avatars",
"help": "A URL for an avatar .fst to be used when someone tries to use an avatar that is not allowed. If left blank, the generic default avatar is used.",
"placeholder": "",
"default": "",
"advanced": true
}
]
},
{
"name": "audio_threading",
"label": "Audio Threading",
@ -1080,6 +986,7 @@
"name": "attenuation_per_doubling_in_distance",
"label": "Default Domain Attenuation",
"help": "Factor between 0 and 1.0 (0: No attenuation, 1.0: extreme attenuation)",
"content_setting": true,
"placeholder": "0.5",
"default": "0.5",
"advanced": false
@ -1105,6 +1012,7 @@
"label": "Zones",
"help": "In this table you can define a set of zones in which you can specify various audio properties.",
"numbered": false,
"content_setting": true,
"can_add_new_rows": true,
"key": {
"name": "name",
@ -1155,6 +1063,7 @@
"type": "table",
"label": "Attenuation Coefficients",
"help": "In this table you can set custom attenuation coefficients between audio zones",
"content_setting": true,
"numbered": true,
"can_order": true,
"can_add_new_rows": true,
@ -1185,6 +1094,7 @@
"label": "Reverb Settings",
"help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.",
"numbered": true,
"content_setting": true,
"can_add_new_rows": true,
"columns": [
{
@ -1266,9 +1176,82 @@
}
]
},
{
"name": "avatars",
"label": "Avatars",
"assignment-types": [ 1, 2 ],
"settings": [
{
"name": "min_avatar_height",
"type": "double",
"label": "Minimum Avatar Height (meters)",
"help": "Limits the height of avatars in your domain. Must be at least 0.009.",
"placeholder": 0.4,
"default": 0.4
},
{
"name": "max_avatar_height",
"type": "double",
"label": "Maximum Avatar Height (meters)",
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1755.",
"placeholder": 5.2,
"default": 5.2
},
{
"name": "avatar_whitelist",
"label": "Avatars Allowed from:",
"help": "Comma separated list of URLs (with optional paths) that avatar .fst files are allowed from. If someone attempts to use an avatar with a different domain, it will be rejected and the replacement avatar will be used. If left blank, any domain is allowed.",
"placeholder": "",
"default": "",
"advanced": true
},
{
"name": "replacement_avatar",
"label": "Replacement Avatar for disallowed avatars",
"help": "A URL for an avatar .fst to be used when someone tries to use an avatar that is not allowed. If left blank, the generic default avatar is used.",
"placeholder": "",
"default": "",
"advanced": true
}
]
},
{
"name": "avatar_mixer",
"label": "Avatar Mixer",
"assignment-types": [
1
],
"settings": [
{
"name": "max_node_send_bandwidth",
"type": "double",
"label": "Per-Node Bandwidth",
"help": "Desired maximum send bandwidth (in Megabits per second) to each node",
"placeholder": 5.0,
"default": 5.0,
"advanced": true
},
{
"name": "auto_threads",
"label": "Automatically determine thread count",
"type": "checkbox",
"help": "Allow system to determine number of threads (recommended)",
"default": false,
"advanced": true
},
{
"name": "num_threads",
"label": "Number of Threads",
"help": "Threads to spin up for avatar mixing (if not automatically set)",
"placeholder": "1",
"default": "1",
"advanced": true
}
]
},
{
"name": "entity_server_settings",
"label": "Entity Server Settings",
"label": "Entities",
"assignment-types": [
6
],
@ -1309,6 +1292,7 @@
"name": "entityEditFilter",
"label": "Filter Entity Edits",
"help": "Check all entity edits against this filter function.",
"content_setting": true,
"placeholder": "url whose content is like: function filter(properties) { return properties; }",
"default": "",
"advanced": true
@ -1503,35 +1487,55 @@
]
},
{
"name": "avatar_mixer",
"label": "Avatar Mixer",
"assignment-types": [
1
],
"name": "asset_server",
"label": "Asset Server (ATP)",
"assignment-types": [ 3 ],
"settings": [
{
"name": "max_node_send_bandwidth",
"type": "double",
"label": "Per-Node Bandwidth",
"help": "Desired maximum send bandwidth (in Megabits per second) to each node",
"placeholder": 5.0,
"default": 5.0,
"advanced": true
},
{
"name": "auto_threads",
"label": "Automatically determine thread count",
"name": "enabled",
"type": "checkbox",
"help": "Allow system to determine number of threads (recommended)",
"default": false,
"label": "Enabled",
"help": "Assigns an asset-server in your domain to serve files to clients via the ATP protocol (over UDP)",
"default": true,
"advanced": true
},
{
"name": "num_threads",
"label": "Number of Threads",
"help": "Threads to spin up for avatar mixing (if not automatically set)",
"placeholder": "1",
"default": "1",
"name": "assets_path",
"type": "string",
"label": "Assets Path",
"help": "The path to the directory assets are stored in.<br/>If this path is relative, it will be relative to the application data directory.<br/>If you change this path you will need to manually copy any existing assets from the previous directory.",
"default": "",
"advanced": true
},
{
"name": "assets_filesize_limit",
"type": "int",
"label": "File Size Limit",
"help": "The file size limit of an asset that can be imported into the asset server in MBytes. 0 (default) means no limit on file size.",
"default": 0,
"advanced": true
}
]
},
{
"name": "entity_script_server",
"label": "Entity Script Server (ESS)",
"assignment-types": [ 5 ],
"settings": [
{
"name": "entity_pps_per_script",
"label": "Entity PPS per script",
"help": "The number of packets per second (PPS) that can be sent to the entity server for each server entity script. This contributes to a total overall amount.<br/>Example: 1000 PPS with 5 entites gives a total PPS of 5000 that is shared among the entity scripts. A single could use 4000 PPS, leaving 1000 for the rest, for example.",
"default": 900,
"type": "int",
"advanced": true
},
{
"name": "max_total_entity_pps",
"label": "Maximum Total Entity PPS",
"help": "The maximum total packets per seconds (PPS) that can be sent to the entity server.<br/>Example: 5 scripts @ 1000 PPS per script = 5000 total PPS. A maximum total PPS of 4000 would cap this 5000 total PPS to 4000.",
"default": 9000,
"type": "int",
"advanced": true
}
]

View file

@ -0,0 +1,7 @@
<script src='/js/underscore-min.js'></script>
<script src='/js/underscore-keypath.min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='/js/form2js.min.js'></script>
<script src='/js/bootstrap-switch.min.js'></script>
<script src='/js/shared.js'></script>
<script src='/js/base-settings.js'></script>

View file

@ -0,0 +1,59 @@
<div class="col-md-10 col-md-offset-1 col-xs-12">
<div class="row">
<div class="col-xs-12">
<div id="cloud-domains-alert" class="alert alert-info alert-dismissible" role="alert" style="display: none;">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span class="alert-link">
<a href="https://highfidelity.com/user/cloud_domains" target="_blank" class="blue-link">Visit Cloud Hosted Domains</a> to manage all your cloud domains
</span>
</div>
<form id="settings-form" role="form">
<script id="panels-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% if (!group.hidden) { %>
<% var settings = _.partition(group.settings, function(value, index) { %>
<% return !value.deprecated %>
<% })[0] %>
<% var split_settings = _.partition(group.settings, function(value, index) { %>
<% return !value.advanced %>
<% }) %>
<% isGrouped = !!group.name %>
<% panelID = isGrouped ? group.name : group.html_id %>
<div id="<%- panelID %>_group" class="anchor"></div>
<div class="panel panel-default<%- (isGrouped) ? ' grouped' : '' %>"
id="<%- panelID %>">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
<span class="badge"></span>
</div>
<div class="panel-body">
<% _.each(split_settings[0], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, false) %>
<% }); %>
<% if (split_settings[1].length > 0) { %>
<button type="button" class="btn btn-default" data-toggle="collapse" data-target="#<%- panelID %>-advanced">Advanced Settings <span class="caret"></span></button>
<div id="<%- panelID %>-advanced" class="collapse advanced-settings-section">
<% _.each(split_settings[1], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, true) %>
<% }); %>
</div>
<% } %>
</div>
</div>
<% } %>
<% }); %>
</script>
<div id="panels"></div>
</form>
</div>
</div>
</div>

View file

@ -1,45 +1,19 @@
<!--#include virtual="header.html"-->
<div class="col-md-10 col-md-offset-1">
<div class="row">
<div class="col-xs-12">
<div class="alert" style="display:none;"></div>
</div>
</div>
<script type="text/javascript">
var Settings = {
content_settings: true,
endpoint: "/content-settings.json",
path: "/content/"
};
</script>
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Upload Entities File</h3>
</div>
<form id="upload-form" action="upload" enctype="multipart/form-data" method="post">
<div class="panel-body">
<p>
Upload an entities file (e.g.: models.json.gz) to replace the content of this domain.<br>
Note: <strong>Your domain's content will be replaced by the content you upload</strong>, but the backup files of your domain's content will not immediately be changed.
</p>
<p>If your domain has any content that you would like to re-use at a later date, save a manual backup of your models.json.gz file, which is usually stored at the following paths:</p>
<label class="control-label">Windows</label>
<pre>C:/Users/[username]/AppData/Roaming/High Fidelity/assignment-client/entities/models.json.gz</pre>
<label class="control-label">OSX</label>
<pre>/Users/[username]/Library/Application Support/High Fidelity/assignment-client/entities/models.json.gz</pre>
<label class="control-label">Linux</label>
<pre>/home/[username]/.local/share/High Fidelity/assignment-client/entities/models.json.gz</pre>
<br>
<input type="file" name="entities-file" class="form-control-file" accept=".json, .gz">
<br>
</div>
<div class="panel-footer">
<input type="submit" class="btn btn-info" value="Upload">
</div>
</form>
</div>
</div>
</div>
</div>
<!--#include virtual="base-settings.html"-->
<!--#include virtual="footer.html"-->
<script src='js/content.js'></script>
<script src='/js/sweetalert.min.js'></script>
<!--#include virtual="base-settings-scripts.html"-->
<script src="js/content.js"></script>
<!--#include virtual="page-end.html"-->

View file

@ -1,14 +1,6 @@
$(document).ready(function(){
function showSpinnerAlert(title) {
swal({
title: title,
text: '<div class="spinner" style="color:black;"><div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div></div>',
html: true,
showConfirmButton: false,
allowEscapeKey: false
});
}
Settings.afterReloadActions = function() {};
var frm = $('#upload-form');
frm.submit(function (ev) {

View file

@ -18,6 +18,14 @@ body {
margin-top: 70px;
}
/* unfortunate hack so that anchors go to the right place with fixed navbar */
:target:before {
content: " ";
display: block;
height: 70px;
margin-top: -70px;
}
[hidden] {
display: none !important;
}
@ -118,11 +126,6 @@ span.port {
margin-top: 10px;
}
#small-save-button {
width: 100%;
margin-bottom: 15px;
}
td.buttons {
width: 30px;
}
@ -345,3 +348,89 @@ table .headers + .headers td {
text-align: center;
margin-top: 20px;
}
@media (min-width: 768px) {
ul.nav li.dropdown-on-hover:hover ul.dropdown-menu {
display: block;
}
}
ul.nav li.dropdown ul.dropdown-menu {
padding: 0px 0px;
}
ul.nav li.dropdown li a {
padding-top: 7px;
padding-bottom: 7px;
}
ul.nav li.dropdown li a:hover {
color: white;
background-color: #337ab7;
}
ul.nav li.dropdown ul.dropdown-menu .divider {
margin: 0px 0;
}
#visit-domain-link {
background-color: transparent;
}
.navbar-btn {
margin-left: 10px;
}
#save-settings-xs-button {
float: right;
margin-right: 10px;
}
#button-bars {
display: inline-block;
float: left;
}
#hamburger-badge {
position: relative;
top: -2px;
float: left;
margin-right: 10px;
margin-left: 0px;
}
#restart-server {
margin-left: 0px;
}
#restart-server:hover {
text-decoration: none;
}
.badge {
margin-left: 5px;
background-color: #00B4EF !important;
}
.panel-title {
display: inline-block;
}
#visit-hmd-icon {
width: 25px;
position: relative;
top: -1px;
}
.advanced-settings-section {
margin-top: 20px;
}
#restore-settings-button {
margin-top: 10px;
}
/* fix for https://bugs.webkit.org/show_bug.cgi?id=39620 */
.save-button-text {
pointer-events: none;
}

View file

@ -17,30 +17,47 @@
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#collapsed-navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span id="hamburger-badge" class="badge"></span>
<div id="button-bars">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</div>
</button>
<button id="save-settings-xs-button" class="save-button btn btn-success navbar-btn hidden-sm hidden-md hidden-lg" disabled="true"><span class="save-button-text">Save</span></button>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<div class="collapse navbar-collapse" id="collapsed-navbar">
<ul class="nav navbar-nav">
<li><a href="/">Nodes</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assignments <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="/assignment">New Assignment</a></li>
</ul>
<li><a href="/assignment">Assignment</a></li>
<li class="dropdown dropdown-on-hover">
<a href="/content/" class="hidden-xs">Content <span class="content-settings-badge badge"></span> <span class="caret"></span></a>
<a href="#" class="dropdown-toggle hidden-sm hidden-md hidden-lg" data-toggle="dropdown">Content <span class="content-badge badge"></span> <span class="caret"></span></a>
<ul id="content-settings-nav-dropdown" class="dropdown-menu" role="menu">
</ul>
</li>
<li class="dropdown dropdown-on-hover">
<a href="/settings/" class="hidden-xs">Settings <span class="domain-settings-badge badge"></span> <span class="caret"></span></a>
<a href="#" class="dropdown-toggle hidden-sm hidden-md hidden-lg" data-toggle="dropdown">Settings <span class="domain-settings-badge badge"></span> <span class="caret"></span></a>
<ul id="domain-settings-nav-dropdown" class="dropdown-menu" role="menu">
</ul>
</li>
<li><a href="/content/">Content</a></li>
<li><a href="/settings/">Settings</a></li>
</ul>
<ul class="nav navbar-right navbar-nav">
<li><a id="visit-domain-link" class="blue-link" target="_blank" style="display: none;">Visit domain in VR</a></li>
<li><a href="#" id="restart-server"><span class="glyphicon glyphicon-refresh"></span> Restart</a></li>
<a id="visit-domain-link" class="btn btn-default navbar-btn" role="button" target="_blank" style="display: none;">
<img id="visit-hmd-icon" src="/images/hmd-w-eyes.svg" alt="Head-mounted display" />
Visit in VR
</a>
<button id="save-settings-button" class="save-button btn btn-success navbar-btn hidden-xs" disabled="true"><span class="save-button-text">Save</span></button>
<a href="#" id="restart-server" class="navbar-btn btn btn-link"><span class="glyphicon glyphicon-refresh"></span> Restart</a>
</ul>
</div>
</div><!-- /.container-fluid -->
@ -50,7 +67,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">domain-server is restarting</h4>
<h4 class="modal-title">Domain Server is restarting</h4>
</div>
<div class="modal-body">
<h5>This page will automatically refresh in <span id="refresh-time">3 seconds</span>.</h5>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 456 244" style="enable-background:new 0 0 456 244;" xml:space="preserve">
<style type="text/css">
.st0{fill:#666666;}
</style>
<path class="st0" d="M352.8,3.4h-250C49.6,3.4,6.3,46.2,6.3,98.9v44.9c0,52.7,43.3,95.5,96.5,95.5h67.6c19.4,0,31.9-17.9,42.9-33.6
c4.2-6.1,11.1-15.9,14.8-17.9c3.6,2.1,10.4,12.2,14.5,18.4c10.4,15.5,22.1,33.1,40.3,33.1h69.9c53.3,0,96.5-42.9,96.5-95.5V98.9
C449.3,46.2,406,3.4,352.8,3.4z M129.6,157.6c-22.4,0-40.6-18.2-40.6-40.6s18.2-40.6,40.6-40.6c22.4,0,40.6,18.2,40.6,40.6
S151.9,157.6,129.6,157.6z M328.4,157.6c-22.4,0-40.6-18.2-40.6-40.6s18.2-40.6,40.6-40.6c22.4,0,40.6,18.2,40.6,40.6
S350.8,157.6,328.4,157.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 928 B

File diff suppressed because it is too large Load diff

View file

@ -23,16 +23,22 @@ function showRestartModal() {
}, 1000);
}
function settingsGroupAnchor(base, html_id) {
return base + "#" + html_id + "_group"
}
$(document).ready(function(){
var url = window.location;
// Will only work if string in href matches with location
$('ul.nav a[href="'+ url +'"]').parent().addClass('active');
// Will also work for relative and absolute hrefs
$('ul.nav a').filter(function() {
return this.href == url;
}).parent().addClass('active');
$('body').on('click', '#restart-server', function(e) {
}).parent().addClass('active');
$('body').on('click', '#restart-server', function(e) {
swal( {
title: "Are you sure?",
text: "This will restart your domain server, causing your domain to be briefly offline.",
@ -45,4 +51,47 @@ $(document).ready(function(){
});
return false;
});
var $contentDropdown = $('#content-settings-nav-dropdown');
var $settingsDropdown = $('#domain-settings-nav-dropdown');
// for pages that have the settings dropdowns
if ($contentDropdown.length && $settingsDropdown.length) {
// make a JSON request to get the dropdown menus for content and settings
// we don't error handle here because the top level menu is still clickable and usables if this fails
$.getJSON('/settings-menu-groups.json', function(data){
function makeGroupDropdownElement(group, base) {
var html_id = group.html_id ? group.html_id : group.name;
return "<li class='setting-group'><a href='" + settingsGroupAnchor(base, html_id) + "'>" + group.label + "<span class='badge'></span></a></li>";
}
$.each(data.content_settings, function(index, group){
if (index > 0) {
$contentDropdown.append("<li role='separator' class='divider'></li>");
}
$contentDropdown.append(makeGroupDropdownElement(group, "/content/"));
});
$.each(data.domain_settings, function(index, group){
if (index > 0) {
$settingsDropdown.append("<li role='separator' class='divider'></li>");
}
$settingsDropdown.append(makeGroupDropdownElement(group, "/settings/"));
// for domain settings, we add a dummy "Places" group that we fill
// via the API - add it to the dropdown menu in the right spot
// which is after "Metaverse / Networking"
if (group.name == "metaverse") {
$settingsDropdown.append("<li role='separator' class='divider'></li>");
$settingsDropdown.append(makeGroupDropdownElement({ html_id: 'places', label: 'Places' }, "/settings/"));
}
});
// append a link for the "Settings Backup" panel
$settingsDropdown.append("<li role='separator' class='divider'></li>");
$settingsDropdown.append(makeGroupDropdownElement({ html_id: 'settings_backup', label: 'Settings Backup'}, "/settings"));
});
}
});

View file

@ -1,6 +1,8 @@
var Settings = {
showAdvanced: false,
ADVANCED_CLASS: 'advanced-setting',
if (typeof Settings === "undefined") {
Settings = {};
}
Object.assign(Settings, {
DEPRECATED_CLASS: 'deprecated-setting',
TRIGGER_CHANGE_CLASS: 'trigger-change',
DATA_ROW_CLASS: 'value-row',
@ -41,7 +43,7 @@ var Settings = {
FORM_ID: 'settings-form',
INVALID_ROW_CLASS: 'invalid-input',
DATA_ROW_INDEX: 'data-row-index'
};
});
var URLs = {
// STABLE METAVERSE_URL: https://metaverse.highfidelity.com
@ -95,7 +97,13 @@ var DOMAIN_ID_TYPE_FULL = 2;
var DOMAIN_ID_TYPE_UNKNOWN = 3;
function domainIDIsSet() {
return Settings.data.values.metaverse.id.length > 0;
if (typeof Settings.data.values.metaverse !== 'undefined' &&
typeof Settings.data.values.metaverse.id !== 'undefined') {
return Settings.data.values.metaverse.id.length > 0;
} else {
return false;
}
}
function getCurrentDomainIDType() {
@ -156,11 +164,12 @@ function getDomainFromAPI(callback) {
if (callback === undefined) {
callback = function() {};
}
var domainID = Settings.data.values.metaverse.id;
if (domainID === null || domainID === undefined || domainID === '') {
if (!domainIDIsSet()) {
callback({ status: 'fail' });
return null;
} else {
var domainID = Settings.data.values.metaverse.id;
}
pendingDomainRequest = $.ajax({

File diff suppressed because one or more lines are too long

View file

@ -1,104 +1,29 @@
<!--#include virtual="header.html"-->
<div class="col-md-10 col-md-offset-1">
<div class="row">
<div class="col-md-12">
<div class="alert" style="display:none;"></div>
</div>
</div>
<script type="text/javascript">
var Settings = {
content_settings: false,
endpoint: "/settings.json",
path: "/settings/"
};
</script>
<div class="row">
<div class="col-md-3 col-sm-3" id="setup-sidebar-col">
<div id="setup-sidebar" data-clampedwidth="#setup-sidebar-col">
<script id="list-group-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% if (!group.hidden) { %>
<% panelID = group.name ? group.name : group.html_id %>
<li>
<a href="#<%- panelID %>" class="list-group-item">
<span class="badge"></span>
<%- group.label %>
</a>
</li>
<% } %>
<% }); %>
</script>
<ul class="nav nav-pills nav-stacked">
</ul>
<button id="advanced-toggle-button" class="btn btn-info advanced-toggle">Show advanced</button>
<button class="btn btn-success save-button" disabled>Save</button>
<div id="manage-cloud-domains-link" style="display: none;">
<a href="https://highfidelity.com/user/cloud_domains" target="_blank" class="blue-link">Manage Cloud Hosted Domains</a>
</div>
</div>
</div>
<div class="col-md-9 col-sm-9 col-xs-12">
<div class="col-md-10 col-md-offset-1 col-xs-12">
<div class="row">
<div class="col-xs-12">
<div id="cloud-domains-alert" class="alert alert-info alert-dismissible" role="alert" style="display: none;">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span class="alert-link">
<a href="https://highfidelity.com/user/cloud_domains" target="_blank" class="blue-link">Visit Cloud Hosted Domains</a> to manage all your cloud domains
</span>
</div>
<form id="settings-form" role="form">
<script id="panels-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% if (!group.hidden) { %>
<% var settings = _.partition(group.settings, function(value, index) { return !value.deprecated })[0] %>
<% split_settings = _.partition(settings, function(value, index) { return !value.advanced }) %>
<% isAdvanced = _.isEmpty(split_settings[0]) && !_.isEmpty(split_settings[1]) %>
<% if (isAdvanced) { %>
<% $("a[href=#" + group.name + "]").addClass('advanced-setting').hide() %>
<% } %>
<% isGrouped = !!group.name %>
<% panelID = isGrouped ? group.name : group.html_id %>
<div class="panel panel-default<%- (isAdvanced) ? ' advanced-setting' : '' %><%- (isGrouped) ? ' grouped' : '' %>"
id="<%- panelID %>">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
</div>
<div class="panel-body">
<% _.each(split_settings[0], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, false) %>
<% }); %>
<% if (!_.isEmpty(split_settings[1])) { %>
<% $("#advanced-toggle-button").show() %>
<% _.each(split_settings[1], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, true) %>
<% }); %>
<% }%>
</div>
</div>
<% } %>
<% }); %>
</script>
<div id="panels"></div>
</form>
</div>
</div>
<div class="col-xs-12 hidden-sm hidden-md hidden-lg">
<button class="btn btn-success save-button" id="small-save-button">Save</button>
</div>
<div class="alert alert-info">Your domain content settings are now available in <a href='/content/'>Content</a></div>
</div>
</div>
</div>
<!--#include virtual="base-settings.html"-->
<!--#include virtual="footer.html"-->
<script src='/js/underscore-min.js'></script>
<script src='/js/underscore-keypath.min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='/js/sha256.js'></script>
<script src='js/form2js.min.js'></script>
<script src='js/bootstrap-switch.min.js'></script>
<script src='/js/shared.js'></script>
<script src='js/settings.js'></script>
<!--#include virtual="base-settings-scripts.html"-->
<script src="js/settings.js"></script>
<!--#include virtual="page-end.html"-->

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,400 @@
//
// BackupSupervisor.cpp
// domain-server/src
//
// Created by Clement Brisset on 1/12/18.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "BackupSupervisor.h"
#include <QJsonDocument>
#include <QDate>
#include <AssetClient.h>
#include <AssetRequest.h>
#include <AssetUpload.h>
#include <MappingRequest.h>
#include <PathUtils.h>
const QString BACKUPS_DIR = "backups/";
const QString ASSETS_DIR = "files/";
const QString MAPPINGS_PREFIX = "mappings-";
using namespace std;
BackupSupervisor::BackupSupervisor() {
_backupsDirectory = PathUtils::getAppDataPath() + BACKUPS_DIR;
QDir backupDir { _backupsDirectory };
if (!backupDir.exists()) {
backupDir.mkpath(".");
}
_assetsDirectory = PathUtils::getAppDataPath() + BACKUPS_DIR + ASSETS_DIR;
QDir assetsDir { _assetsDirectory };
if (!assetsDir.exists()) {
assetsDir.mkpath(".");
}
loadAllBackups();
}
void BackupSupervisor::loadAllBackups() {
_backups.clear();
_assetsInBackups.clear();
_assetsOnDisk.clear();
_allBackupsLoadedSuccessfully = true;
QDir assetsDir { _assetsDirectory };
auto assetNames = assetsDir.entryList(QDir::Files);
qDebug() << "Loading" << assetNames.size() << "assets.";
// store all valid hashes
copy_if(begin(assetNames), end(assetNames),
inserter(_assetsOnDisk, begin(_assetsOnDisk)), AssetUtils::isValidHash);
QDir backupsDir { _backupsDirectory };
auto files = backupsDir.entryList({ MAPPINGS_PREFIX + "*.json" }, QDir::Files);
qDebug() << "Loading" << files.size() << "backups.";
for (const auto& fileName : files) {
auto filePath = backupsDir.filePath(fileName);
auto success = loadBackup(filePath);
if (!success) {
qCritical() << "Failed to load backup file" << filePath;
_allBackupsLoadedSuccessfully = false;
}
}
vector<AssetUtils::AssetHash> missingAssets;
set_difference(begin(_assetsInBackups), end(_assetsInBackups),
begin(_assetsOnDisk), end(_assetsOnDisk),
back_inserter(missingAssets));
if (missingAssets.size() > 0) {
qWarning() << "Found" << missingAssets.size() << "assets missing.";
}
vector<AssetUtils::AssetHash> deprecatedAssets;
set_difference(begin(_assetsOnDisk), end(_assetsOnDisk),
begin(_assetsInBackups), end(_assetsInBackups),
back_inserter(deprecatedAssets));
if (deprecatedAssets.size() > 0) {
qDebug() << "Found" << deprecatedAssets.size() << "assets to delete.";
if (_allBackupsLoadedSuccessfully) {
for (const auto& hash : deprecatedAssets) {
QFile::remove(_assetsDirectory + hash);
}
} else {
qWarning() << "Some backups did not load properly, aborting deleting for safety.";
}
}
}
bool BackupSupervisor::loadBackup(const QString& backupFile) {
_backups.push_back({ backupFile.toStdString(), {}, false });
auto& backup = _backups.back();
QFile file { backupFile };
if (!file.open(QFile::ReadOnly)) {
qCritical() << "Could not open backup file:" << backupFile;
backup.corruptedBackup = true;
return false;
}
QJsonParseError error;
auto document = QJsonDocument::fromJson(file.readAll(), &error);
if (document.isNull() || !document.isObject()) {
qCritical() << "Could not parse backup file to JSON object:" << backupFile;
qCritical() << " Error:" << error.errorString();
backup.corruptedBackup = true;
return false;
}
auto jsonObject = document.object();
for (auto it = begin(jsonObject); it != end(jsonObject); ++it) {
const auto& assetPath = it.key();
const auto& assetHash = it.value().toString();
if (!AssetUtils::isValidHash(assetHash)) {
qCritical() << "Corrupted mapping in backup file" << backupFile << ":" << it.key();
backup.corruptedBackup = true;
return false;
}
backup.mappings[assetPath] = assetHash;
_assetsInBackups.insert(assetHash);
}
_backups.push_back(backup);
return true;
}
void BackupSupervisor::backupAssetServer() {
if (backupInProgress() || restoreInProgress()) {
qWarning() << "There is already a backup/restore in progress.";
return;
}
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createGetAllMappingsRequest();
connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) {
qDebug() << "Got" << request->getMappings().size() << "mappings!";
if (request->getError() != MappingRequest::NoError) {
qCritical() << "Could not complete backup.";
qCritical() << " Error:" << request->getErrorString();
finishBackup();
request->deleteLater();
return;
}
if (!writeBackupFile(request->getMappings())) {
finishBackup();
request->deleteLater();
return;
}
assert(!_backups.empty());
const auto& mappings = _backups.back().mappings;
backupMissingFiles(mappings);
request->deleteLater();
});
startBackup();
request->start();
}
void BackupSupervisor::backupMissingFiles(const AssetUtils::Mappings& mappings) {
_assetsLeftToRequest.reserve(mappings.size());
for (auto& mapping : mappings) {
const auto& hash = mapping.second;
if (_assetsOnDisk.find(hash) == end(_assetsOnDisk)) {
_assetsLeftToRequest.push_back(hash);
}
}
backupNextMissingFile();
}
void BackupSupervisor::backupNextMissingFile() {
if (_assetsLeftToRequest.empty()) {
finishBackup();
return;
}
auto hash = _assetsLeftToRequest.back();
_assetsLeftToRequest.pop_back();
auto assetClient = DependencyManager::get<AssetClient>();
auto assetRequest = assetClient->createRequest(hash);
connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) {
if (request->getError() == AssetRequest::NoError) {
qDebug() << "Got" << request->getHash();
bool success = writeAssetFile(request->getHash(), request->getData());
if (!success) {
qCritical() << "Failed to write asset file" << request->getHash();
}
} else {
qCritical() << "Failed to backup asset" << request->getHash();
}
backupNextMissingFile();
request->deleteLater();
});
assetRequest->start();
}
bool BackupSupervisor::writeBackupFile(const AssetUtils::AssetMappings& mappings) {
auto filename = MAPPINGS_PREFIX + QDateTime::currentDateTimeUtc().toString(Qt::ISODate) + ".json";
QFile file { PathUtils::getAppDataPath() + BACKUPS_DIR + filename };
if (!file.open(QFile::WriteOnly)) {
qCritical() << "Could not open backup file" << file.fileName();
return false;
}
AssetServerBackup backup;
QJsonObject jsonObject;
for (auto& mapping : mappings) {
backup.mappings[mapping.first] = mapping.second.hash;
_assetsInBackups.insert(mapping.second.hash);
jsonObject.insert(mapping.first, mapping.second.hash);
}
QJsonDocument document(jsonObject);
file.write(document.toJson());
backup.filePath = file.fileName().toStdString();
_backups.push_back(backup);
return true;
}
bool BackupSupervisor::writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data) {
QDir assetsDir { _assetsDirectory };
QFile file { assetsDir.filePath(hash) };
if (!file.open(QFile::WriteOnly)) {
qCritical() << "Could not open backup file" << file.fileName();
return false;
}
file.write(data);
_assetsOnDisk.insert(hash);
return true;
}
void BackupSupervisor::restoreAssetServer(int backupIndex) {
if (backupInProgress() || restoreInProgress()) {
qWarning() << "There is already a backup/restore in progress.";
return;
}
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createGetAllMappingsRequest();
connect(request, &GetAllMappingsRequest::finished, this, [this, backupIndex](GetAllMappingsRequest* request) {
if (request->getError() == MappingRequest::NoError) {
const auto& newMappings = _backups.at(backupIndex).mappings;
computeServerStateDifference(request->getMappings(), newMappings);
restoreAllAssets();
} else {
finishRestore();
}
request->deleteLater();
});
startRestore();
request->start();
}
void BackupSupervisor::computeServerStateDifference(const AssetUtils::AssetMappings& currentMappings,
const AssetUtils::Mappings& newMappings) {
_mappingsLeftToSet.reserve((int)newMappings.size());
_assetsLeftToUpload.reserve((int)newMappings.size());
_mappingsLeftToDelete.reserve((int)currentMappings.size());
set<AssetUtils::AssetHash> currentAssets;
for (const auto& currentMapping : currentMappings) {
const auto& currentPath = currentMapping.first;
const auto& currentHash = currentMapping.second.hash;
if (newMappings.find(currentPath) == end(newMappings)) {
_mappingsLeftToDelete.push_back(currentPath);
}
currentAssets.insert(currentHash);
}
for (const auto& newMapping : newMappings) {
const auto& newPath = newMapping.first;
const auto& newHash = newMapping.second;
auto it = currentMappings.find(newPath);
if (it == end(currentMappings) || it->second.hash != newHash) {
_mappingsLeftToSet.push_back({ newPath, newHash });
}
if (currentAssets.find(newHash) == end(currentAssets)) {
_assetsLeftToUpload.push_back(newHash);
}
}
qDebug() << "Mappings to set:" << _mappingsLeftToSet.size();
qDebug() << "Mappings to del:" << _mappingsLeftToDelete.size();
qDebug() << "Assets to upload:" << _assetsLeftToUpload.size();
}
void BackupSupervisor::restoreAllAssets() {
restoreNextAsset();
}
void BackupSupervisor::restoreNextAsset() {
if (_assetsLeftToUpload.empty()) {
updateMappings();
return;
}
auto hash = _assetsLeftToUpload.back();
_assetsLeftToUpload.pop_back();
auto assetFilename = _assetsDirectory + hash;
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createUpload(assetFilename);
connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) {
if (request->getError() != AssetUpload::NoError) {
qCritical() << "Failed to restore asset:" << request->getFilename();
qCritical() << " Error:" << request->getErrorString();
}
restoreNextAsset();
request->deleteLater();
});
request->start();
}
void BackupSupervisor::updateMappings() {
auto assetClient = DependencyManager::get<AssetClient>();
for (const auto& mapping : _mappingsLeftToSet) {
auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second);
connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) {
if (request->getError() != MappingRequest::NoError) {
qCritical() << "Failed to set mapping:" << request->getPath();
qCritical() << " Error:" << request->getErrorString();
}
if (--_mappingRequestsInFlight == 0) {
finishRestore();
}
request->deleteLater();
});
request->start();
++_mappingRequestsInFlight;
}
_mappingsLeftToSet.clear();
auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete);
connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) {
if (request->getError() != MappingRequest::NoError) {
qCritical() << "Failed to delete mappings";
qCritical() << " Error:" << request->getErrorString();
}
if (--_mappingRequestsInFlight == 0) {
finishRestore();
}
request->deleteLater();
});
_mappingsLeftToDelete.clear();
request->start();
++_mappingRequestsInFlight;
}
bool BackupSupervisor::deleteBackup(int backupIndex) {
if (backupInProgress() || restoreInProgress()) {
qWarning() << "There is a backup/restore in progress.";
return false;
}
const auto& filePath = _backups.at(backupIndex).filePath;
auto success = QFile::remove(filePath.c_str());
loadAllBackups();
return success;
}

View file

@ -0,0 +1,85 @@
//
// BackupSupervisor.h
// domain-server/src
//
// Created by Clement Brisset on 1/12/18.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_BackupSupervisor_h
#define hifi_BackupSupervisor_h
#include <set>
#include <map>
#include <QObject>
#include <AssetUtils.h>
#include <ReceivedMessage.h>
struct AssetServerBackup {
std::string filePath;
AssetUtils::Mappings mappings;
bool corruptedBackup;
};
class BackupSupervisor : public QObject {
Q_OBJECT
public:
BackupSupervisor();
void backupAssetServer();
void restoreAssetServer(int backupIndex);
bool deleteBackup(int backupIndex);
const std::vector<AssetServerBackup>& getBackups() const { return _backups; };
bool backupInProgress() const { return _backupInProgress; }
bool restoreInProgress() const { return _restoreInProgress; }
private:
void loadAllBackups();
bool loadBackup(const QString& backupFile);
void startBackup() { _backupInProgress = true; }
void finishBackup() { _backupInProgress = false; }
void backupMissingFiles(const AssetUtils::Mappings& mappings);
void backupNextMissingFile();
bool writeBackupFile(const AssetUtils::AssetMappings& mappings);
bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data);
void startRestore() { _restoreInProgress = true; }
void finishRestore() { _restoreInProgress = false; }
void computeServerStateDifference(const AssetUtils::AssetMappings& currentMappings,
const AssetUtils::Mappings& newMappings);
void restoreAllAssets();
void restoreNextAsset();
void updateMappings();
QString _backupsDirectory;
QString _assetsDirectory;
// Internal storage for backups on disk
bool _allBackupsLoadedSuccessfully { false };
std::vector<AssetServerBackup> _backups;
std::set<AssetUtils::AssetHash> _assetsInBackups;
std::set<AssetUtils::AssetHash> _assetsOnDisk;
// Internal storage for backup in progress
bool _backupInProgress { false };
std::vector<AssetUtils::AssetHash> _assetsLeftToRequest;
// Internal storage for restore in progress
bool _restoreInProgress { false };
std::vector<AssetUtils::AssetHash> _assetsLeftToUpload;
std::vector<std::pair<AssetUtils::AssetPath, AssetUtils::AssetHash>> _mappingsLeftToSet;
AssetUtils::AssetPathList _mappingsLeftToDelete;
int _mappingRequestsInFlight { 0 };
};
#endif /* hifi_BackupSupervisor_h */

View file

@ -26,6 +26,7 @@
#include <QCommandLineParser>
#include <AccountManager.h>
#include <AssetClient.h>
#include <BuildInfo.h>
#include <DependencyManager.h>
#include <HifiConfigVariantMap.h>
@ -343,6 +344,12 @@ void DomainServer::parseCommandLine() {
DomainServer::~DomainServer() {
qInfo() << "Domain Server is shutting down.";
// cleanup the AssetClient thread
DependencyManager::destroy<AssetClient>();
_assetClientThread.quit();
_assetClientThread.wait();
// destroy the LimitedNodeList before the DomainServer QCoreApplication is down
DependencyManager::destroy<LimitedNodeList>();
}
@ -494,7 +501,7 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) {
// store the new domain ID and auto network setting immediately
QString newSettingsJSON = QString("{\"metaverse\": { \"id\": \"%1\", \"automatic_networking\": \"full\"}}").arg(id);
auto settingsDocument = QJsonDocument::fromJson(newSettingsJSON.toUtf8());
_settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object());
_settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object(), DomainSettings);
// store the new ID and auto networking setting on disk
_settingsManager.persistToFile();
@ -684,11 +691,17 @@ void DomainServer::setupNodeListAndAssignments() {
packetReceiver.registerListener(PacketType::ICEServerHeartbeatDenied, this, "processICEServerHeartbeatDenialPacket");
packetReceiver.registerListener(PacketType::ICEServerHeartbeatACK, this, "processICEServerHeartbeatACK");
// add whatever static assignments that have been parsed to the queue
addStaticAssignmentsToQueue();
// set a custom packetVersionMatch as the verify packet operator for the udt::Socket
nodeList->setPacketFilterOperator(&DomainServer::isPacketVerified);
_assetClientThread.setObjectName("AssetClient Thread");
auto assetClient = DependencyManager::set<AssetClient>();
assetClient->moveToThread(&_assetClientThread);
QObject::connect(&_assetClientThread, &QThread::started, assetClient.data(), &AssetClient::init);
_assetClientThread.start();
// add whatever static assignments that have been parsed to the queue
addStaticAssignmentsToQueue();
}
bool DomainServer::resetAccountManagerAccessToken() {

View file

@ -18,6 +18,7 @@
#include <QtCore/QQueue>
#include <QtCore/QSharedPointer>
#include <QtCore/QStringList>
#include <QtCore/QThread>
#include <QtCore/QUrl>
#include <QAbstractNativeEventFilter>
@ -25,6 +26,7 @@
#include <HTTPSConnection.h>
#include <LimitedNodeList.h>
#include "BackupSupervisor.h"
#include "DomainGatekeeper.h"
#include "DomainMetadata.h"
#include "DomainServerSettingsManager.h"
@ -251,6 +253,8 @@ private:
bool _sendICEServerAddressToMetaverseAPIRedo { false };
QHash<QUuid, QPointer<HTTPSConnection>> _pendingOAuthConnections;
QThread _assetClientThread;
};

View file

@ -39,8 +39,11 @@ const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings
const QString DESCRIPTION_SETTINGS_KEY = "settings";
const QString SETTING_DEFAULT_KEY = "default";
const QString DESCRIPTION_NAME_KEY = "name";
const QString DESCRIPTION_GROUP_LABEL_KEY = "label";
const QString DESCRIPTION_BACKUP_FLAG_KEY = "backup";
const QString SETTING_DESCRIPTION_TYPE_KEY = "type";
const QString DESCRIPTION_COLUMNS_KEY = "columns";
const QString CONTENT_SETTING_FLAG_KEY = "content_setting";
const QString SETTINGS_VIEWPOINT_KEY = "viewpoint";
@ -63,6 +66,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() {
if (descriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) {
_descriptionArray = descriptionDocument.object()[DESCRIPTION_SETTINGS_KEY].toArray();
splitSettingsDescription();
return;
}
}
@ -78,11 +83,99 @@ DomainServerSettingsManager::DomainServerSettingsManager() {
Q_ARG(int, MISSING_SETTINGS_DESC_ERROR_CODE));
}
void DomainServerSettingsManager::splitSettingsDescription() {
// construct separate description arrays for domain settings and content settings
// since they are displayed on different pages
// along the way we also construct one object that holds the groups separated by domain settings
// and content settings, so that the DS can setup dropdown menus below "Content" and "Settings"
// headers to jump directly to a settings group on the page of either
QJsonArray domainSettingsMenuGroups;
QJsonArray contentSettingsMenuGroups;
foreach(const QJsonValue& group, _descriptionArray) {
QJsonObject groupObject = group.toObject();
static const QString HIDDEN_GROUP_KEY = "hidden";
bool groupHidden = groupObject.contains(HIDDEN_GROUP_KEY) && groupObject[HIDDEN_GROUP_KEY].toBool();
QJsonArray domainSettingArray;
QJsonArray contentSettingArray;
foreach(const QJsonValue& settingDescription, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) {
QJsonObject settingDescriptionObject = settingDescription.toObject();
bool isContentSetting = settingDescriptionObject.contains(CONTENT_SETTING_FLAG_KEY)
&& settingDescriptionObject[CONTENT_SETTING_FLAG_KEY].toBool();
if (isContentSetting) {
// push the setting description to the pending content setting array
contentSettingArray.push_back(settingDescriptionObject);
} else {
// push the setting description to the pending domain setting array
domainSettingArray.push_back(settingDescriptionObject);
}
}
if (!domainSettingArray.isEmpty() || !contentSettingArray.isEmpty()) {
// we know for sure we'll have something to add to our settings menu groups
// so setup that object for the group now, as long as the group isn't hidden alltogether
QJsonObject settingsDropdownGroup;
if (!groupHidden) {
if (groupObject.contains(DESCRIPTION_NAME_KEY)) {
settingsDropdownGroup[DESCRIPTION_NAME_KEY] = groupObject[DESCRIPTION_NAME_KEY];
}
settingsDropdownGroup[DESCRIPTION_GROUP_LABEL_KEY] = groupObject[DESCRIPTION_GROUP_LABEL_KEY];
static const QString DESCRIPTION_GROUP_HTML_ID_KEY = "html_id";
if (groupObject.contains(DESCRIPTION_GROUP_HTML_ID_KEY)) {
settingsDropdownGroup[DESCRIPTION_GROUP_HTML_ID_KEY] = groupObject[DESCRIPTION_GROUP_HTML_ID_KEY];
}
}
if (!domainSettingArray.isEmpty()) {
// we have some domain settings from this group, add the group with the filtered settings
QJsonObject filteredGroupObject = groupObject;
filteredGroupObject[DESCRIPTION_SETTINGS_KEY] = domainSettingArray;
_domainSettingsDescription.push_back(filteredGroupObject);
// if the group isn't hidden, add its information to the domain settings menu groups
if (!groupHidden) {
domainSettingsMenuGroups.push_back(settingsDropdownGroup);
}
}
if (!contentSettingArray.isEmpty()) {
// we have some content settings from this group, add the group with the filtered settings
QJsonObject filteredGroupObject = groupObject;
filteredGroupObject[DESCRIPTION_SETTINGS_KEY] = contentSettingArray;
_contentSettingsDescription.push_back(filteredGroupObject);
// if the group isn't hidden, add its information to the content settings menu groups
if (!groupHidden) {
contentSettingsMenuGroups.push_back(settingsDropdownGroup);
}
}
}
}
// populate the settings menu groups with what we've collected
static const QString SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY = "domain_settings";
static const QString SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY = "content_settings";
_settingsMenuGroups[SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY] = domainSettingsMenuGroups;
_settingsMenuGroups[SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY] = contentSettingsMenuGroups;
}
void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer<ReceivedMessage> message) {
Assignment::Type type;
message->readPrimitive(&type);
QJsonObject responseObject = responseObjectForType(QString::number(type));
QJsonObject responseObject = settingsResponseObjectForType(QString::number(type));
auto json = QJsonDocument(responseObject).toJson();
auto packetList = NLPacketList::create(PacketType::DomainSettings, QByteArray(), true, true);
@ -314,14 +407,14 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
QVariant* avatarMinScale = _configMap.valueForKeyPath(AVATAR_MIN_SCALE_KEYPATH);
if (avatarMinScale) {
float scale = avatarMinScale->toFloat();
_configMap.valueForKeyPath(AVATAR_MIN_HEIGHT_KEYPATH, scale * DEFAULT_AVATAR_HEIGHT);
auto newMinScaleVariant = _configMap.valueForKeyPath(AVATAR_MIN_HEIGHT_KEYPATH, true);
*newMinScaleVariant = avatarMinScale->toFloat() * DEFAULT_AVATAR_HEIGHT;
}
QVariant* avatarMaxScale = _configMap.valueForKeyPath(AVATAR_MAX_SCALE_KEYPATH);
if (avatarMaxScale) {
float scale = avatarMaxScale->toFloat();
_configMap.valueForKeyPath(AVATAR_MAX_HEIGHT_KEYPATH, scale * DEFAULT_AVATAR_HEIGHT);
auto newMaxScaleVariant = _configMap.valueForKeyPath(AVATAR_MAX_HEIGHT_KEYPATH, true);
*newMaxScaleVariant = avatarMaxScale->toFloat() * DEFAULT_AVATAR_HEIGHT;
}
}
@ -986,48 +1079,246 @@ QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QStrin
}
bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection *connection, const QUrl &url) {
if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == SETTINGS_PATH_JSON) {
// this is a POST operation to change one or more settings
QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent());
QJsonObject postedObject = postedDocument.object();
if (connection->requestOperation() == QNetworkAccessManager::PostOperation) {
static const QString SETTINGS_RESTORE_PATH = "/settings/restore";
// we recurse one level deep below each group for the appropriate setting
bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject);
if (url.path() == SETTINGS_PATH_JSON || url.path() == CONTENT_SETTINGS_PATH_JSON) {
// this is a POST operation to change one or more settings
QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent());
QJsonObject postedObject = postedDocument.object();
// store whatever the current _settingsMap is to file
persistToFile();
SettingsType endpointType = url.path() == SETTINGS_PATH_JSON ? DomainSettings : ContentSettings;
// return success to the caller
QString jsonSuccess = "{\"status\": \"success\"}";
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// we recurse one level deep below each group for the appropriate setting
bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject, endpointType);
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
if (restartRequired) {
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
} else {
unpackPermissions();
apiRefreshGroupInformation();
emit updateNodePermissions();
emit settingsUpdated();
// store whatever the current _settingsMap is to file
persistToFile();
// return success to the caller
QString jsonSuccess = "{\"status\": \"success\"}";
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
if (restartRequired) {
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
} else {
unpackPermissions();
apiRefreshGroupInformation();
emit updateNodePermissions();
emit settingsUpdated();
}
return true;
} else if (url.path() == SETTINGS_RESTORE_PATH) {
// this is an JSON settings file restore, ask the HTTPConnection to parse the data
QList<FormData> formData = connection->parseFormData();
bool wasRestoreSuccessful = false;
if (formData.size() > 0 && formData[0].second.size() > 0) {
// take the posted file and convert it to a QJsonObject
auto postedDocument = QJsonDocument::fromJson(formData[0].second);
if (postedDocument.isObject()) {
wasRestoreSuccessful = restoreSettingsFromObject(postedDocument.object(), DomainSettings);
}
}
if (wasRestoreSuccessful) {
// respond with a 200 for success
QString jsonSuccess = "{\"status\": \"success\"}";
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
} else {
// respond with a 400 for failure
connection->respond(HTTPConnection::StatusCode400);
}
return true;
}
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation) {
static const QString SETTINGS_MENU_GROUPS_PATH = "/settings-menu-groups.json";
static const QString SETTINGS_BACKUP_PATH = "/settings/backup.json";
return true;
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) {
// setup a JSON Object with descriptions and non-omitted settings
const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions";
const QString SETTINGS_RESPONSE_VALUE_KEY = "values";
if (url.path() == SETTINGS_PATH_JSON || url.path() == CONTENT_SETTINGS_PATH_JSON) {
QJsonObject rootObject;
rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = _descriptionArray;
rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true);
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
// setup a JSON Object with descriptions and non-omitted settings
const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions";
const QString SETTINGS_RESPONSE_VALUE_KEY = "values";
QJsonObject rootObject;
bool forDomainSettings = (url.path() == SETTINGS_PATH_JSON);
bool forContentSettings = (url.path() == CONTENT_SETTINGS_PATH_JSON);;
rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = forDomainSettings
? _domainSettingsDescription : _contentSettingsDescription;
// grab a domain settings object for all types, filtered for the right class of settings
// and exclude default values
rootObject[SETTINGS_RESPONSE_VALUE_KEY] = settingsResponseObjectForType("", true,
forDomainSettings, forContentSettings,
true);
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
return true;
} else if (url.path() == SETTINGS_MENU_GROUPS_PATH) {
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(_settingsMenuGroups).toJson(), "application/json");
return true;
} else if (url.path() == SETTINGS_BACKUP_PATH) {
// grab the settings backup as an authenticated user
// for the domain settings type only, excluding hidden and default values
auto currentDomainSettingsJSON = settingsResponseObjectForType("", true, true, false, false, true);
// setup headers that tell the client to download the file wth a special name
Headers downloadHeaders;
downloadHeaders.insert("Content-Transfer-Encoding", "binary");
// create a timestamped filename for the backup
const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" };
auto backupFilename = "domain-settings_" + QDateTime::currentDateTime().toString(DATETIME_FORMAT) + ".json";
downloadHeaders.insert("Content-Disposition",
QString("attachment; filename=\"%1\"").arg(backupFilename).toLocal8Bit());
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(currentDomainSettingsJSON).toJson(),
"application/force-download", downloadHeaders);
}
}
return false;
}
QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& typeValue, bool isAuthenticated) {
bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) {
QJsonArray& filteredDescriptionArray = settingsType == DomainSettings
? _domainSettingsDescription : _contentSettingsDescription;
// grab a copy of the current config before restore, so that we can back out if something bad happens during
QVariantMap preRestoreConfig = _configMap.getConfig();
bool shouldCancelRestore = false;
// enumerate through the settings in the description
// if we have one in the restore then use it, otherwise clear it from current settings
foreach(const QJsonValue& descriptionGroupValue, filteredDescriptionArray) {
QJsonObject descriptionGroupObject = descriptionGroupValue.toObject();
QString groupKey = descriptionGroupObject[DESCRIPTION_NAME_KEY].toString();
QJsonArray descriptionGroupSettings = descriptionGroupObject[DESCRIPTION_SETTINGS_KEY].toArray();
// grab the matching group from the restore so we can look at its settings
QJsonObject restoreGroup;
QVariantMap* configGroupMap = nullptr;
if (groupKey.isEmpty()) {
// this is for a setting at the root, use the full object as our restore group
restoreGroup = settingsToRestore;
// the variant map for this "group" is just the config map since there's no group
configGroupMap = &_configMap.getConfig();
} else {
if (settingsToRestore.contains(groupKey)) {
restoreGroup = settingsToRestore[groupKey].toObject();
}
// grab the variant for the group
auto groupMapVariant = _configMap.valueForKeyPath(groupKey);
// if it existed, double check that it is a map - any other value is unexpected and should cancel a restore
if (groupMapVariant) {
if (groupMapVariant->canConvert<QVariantMap>()) {
configGroupMap = static_cast<QVariantMap*>(groupMapVariant->data());
} else {
shouldCancelRestore = true;
break;
}
}
}
foreach(const QJsonValue& descriptionSettingValue, descriptionGroupSettings) {
QJsonObject descriptionSettingObject = descriptionSettingValue.toObject();
// we'll override this setting with the default or what is in the restore as long as
// it isn't specifically excluded from backups
bool isBackedUpSetting = !descriptionSettingObject.contains(DESCRIPTION_BACKUP_FLAG_KEY)
|| descriptionSettingObject[DESCRIPTION_BACKUP_FLAG_KEY].toBool();
if (isBackedUpSetting) {
QString settingName = descriptionSettingObject[DESCRIPTION_NAME_KEY].toString();
// check if we have a matching setting for this in the restore
QJsonValue restoreValue;
if (restoreGroup.contains(settingName)) {
restoreValue = restoreGroup[settingName];
}
// we should create the value for this key path in our current config map
// if we had value in the restore file
bool shouldCreateIfMissing = !restoreValue.isNull();
// get a QVariant pointer to this setting in our config map
QString fullSettingKey = !groupKey.isEmpty()
? groupKey + "." + settingName : settingName;
QVariant* variantValue = _configMap.valueForKeyPath(fullSettingKey, shouldCreateIfMissing);
if (restoreValue.isNull()) {
if (variantValue && !variantValue->isNull() && configGroupMap) {
// we didn't have a value to restore, but there might be a value in the config map
// so we need to remove the value in the config map which will set it back to the default
qDebug() << "Removing" << fullSettingKey << "from settings since it is not in the restored JSON";
configGroupMap->remove(settingName);
}
} else {
// we have a value to restore, use update setting to set it
// we might need to re-grab config group map in case it didn't exist when we looked for it before
// but was created by the call to valueForKeyPath before
if (!configGroupMap) {
auto groupMapVariant = _configMap.valueForKeyPath(groupKey);
if (groupMapVariant && groupMapVariant->canConvert<QVariantMap>()) {
configGroupMap = static_cast<QVariantMap*>(groupMapVariant->data());
} else {
shouldCancelRestore = true;
break;
}
}
qDebug() << "Updating setting" << fullSettingKey << "from restored JSON";
updateSetting(settingName, restoreValue, *configGroupMap, descriptionSettingObject);
}
}
}
if (shouldCancelRestore) {
break;
}
}
if (shouldCancelRestore) {
// if we cancelled the restore, go back to our state before and return false
qDebug() << "Restore cancelled, settings have not been changed";
_configMap.getConfig() = preRestoreConfig;
return false;
} else {
// restore completed, persist the new settings
qDebug() << "Restore completed, persisting restored settings to file";
persistToFile();
return true;
}
}
QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated,
bool includeDomainSettings,
bool includeContentSettings,
bool includeDefaults, bool isForBackup) {
QJsonObject responseObject;
if (!typeValue.isEmpty() || isAuthenticated) {
@ -1036,8 +1327,16 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty
const QString AFFECTED_TYPES_JSON_KEY = "assignment-types";
// enumerate the groups in the description object to find which settings to pass
foreach(const QJsonValue& groupValue, _descriptionArray) {
// only enumerate the requested settings type (domain setting or content setting)
QJsonArray& filteredDescriptionArray = _descriptionArray;
if (includeDomainSettings && !includeContentSettings) {
filteredDescriptionArray = _domainSettingsDescription;
} else if (includeContentSettings && !includeDomainSettings) {
filteredDescriptionArray = _contentSettingsDescription;
}
// enumerate the groups in the potentially filtered object to find which settings to pass
foreach(const QJsonValue& groupValue, filteredDescriptionArray) {
QJsonObject groupObject = groupValue.toObject();
QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString();
QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray();
@ -1045,11 +1344,17 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty
QJsonObject groupResponseObject;
foreach(const QJsonValue& settingValue, groupSettingsArray) {
const QString VALUE_HIDDEN_FLAG_KEY = "value-hidden";
QJsonObject settingObject = settingValue.toObject();
if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool()) {
// consider this setting as long as it isn't hidden
// and either this isn't for a backup or it's a value included in backups
bool includedInBackups = !settingObject.contains(DESCRIPTION_BACKUP_FLAG_KEY)
|| settingObject[DESCRIPTION_BACKUP_FLAG_KEY].toBool();
if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool() && (!isForBackup || includedInBackups)) {
QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray();
if (affectedTypesArray.isEmpty()) {
affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray();
@ -1057,8 +1362,6 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty
if (affectedTypesArray.contains(queryType) ||
(queryType.isNull() && isAuthenticated)) {
// this is a setting we should include in the responseObject
QString settingName = settingObject[DESCRIPTION_NAME_KEY].toString();
// we need to check if the settings map has a value for this setting
@ -1074,28 +1377,31 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty
variantValue = _configMap.value(settingName);
}
QJsonValue result;
// final check for inclusion
// either we include default values or we don't but this isn't a default value
if (includeDefaults || !variantValue.isNull()) {
QJsonValue result;
if (variantValue.isNull()) {
// no value for this setting, pass the default
if (settingObject.contains(SETTING_DEFAULT_KEY)) {
result = settingObject[SETTING_DEFAULT_KEY];
if (variantValue.isNull()) {
// no value for this setting, pass the default
if (settingObject.contains(SETTING_DEFAULT_KEY)) {
result = settingObject[SETTING_DEFAULT_KEY];
} else {
// users are allowed not to provide a default for string values
// if so we set to the empty string
result = QString("");
}
} else {
// users are allowed not to provide a default for string values
// if so we set to the empty string
result = QString("");
result = QJsonValue::fromVariant(variantValue);
}
} else {
result = QJsonValue::fromVariant(variantValue);
}
if (!groupKey.isEmpty()) {
// this belongs in the group object
groupResponseObject[settingName] = result;
} else {
// this is a value that should be at the root
responseObject[settingName] = result;
if (!groupKey.isEmpty()) {
// this belongs in the group object
groupResponseObject[settingName] = result;
} else {
// this is a value that should be at the root
responseObject[settingName] = result;
}
}
}
}
@ -1108,7 +1414,6 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty
}
}
return responseObject;
}
@ -1140,6 +1445,8 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV
settingMap[key] = sanitizedValue;
}
}
} else if (newValue.isDouble()) {
settingMap[key] = newValue.toDouble();
} else if (newValue.isBool()) {
settingMap[key] = newValue.toBool();
} else if (newValue.isObject()) {
@ -1212,7 +1519,8 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson
return QJsonObject();
}
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject,
SettingsType settingsType) {
static const QString SECURITY_ROOT_KEY = "security";
static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist";
static const QString BROADCASTING_KEY = "broadcasting";
@ -1222,6 +1530,8 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
auto& settingsVariant = _configMap.getConfig();
bool needRestart = false;
auto& filteredDescriptionArray = settingsType == DomainSettings ? _domainSettingsDescription : _contentSettingsDescription;
// Iterate on the setting groups
foreach(const QString& rootKey, postedObject.keys()) {
const QJsonValue& rootValue = postedObject[rootKey];
@ -1236,7 +1546,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
QJsonObject groupDescriptionObject;
// we need to check the description array to see if this is a root setting or a group setting
foreach(const QJsonValue& groupValue, _descriptionArray) {
foreach(const QJsonValue& groupValue, filteredDescriptionArray) {
if (groupValue.toObject()[DESCRIPTION_NAME_KEY] == rootKey) {
// we matched a group - keep this since we'll use it below to update the settings
groupDescriptionObject = groupValue.toObject();
@ -1257,7 +1567,9 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
foreach(const QJsonValue& groupValue, _descriptionArray) {
// find groups with root values (they don't have a group name)
QJsonObject groupObject = groupValue.toObject();
if (!groupObject.contains(DESCRIPTION_NAME_KEY)) {
// this is a group with root values - check if our setting is in here
matchingDescriptionObject = settingDescriptionFromGroup(groupObject, rootKey);
@ -1269,6 +1581,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY &&
rootKey != SETTINGS_PATHS_KEY && rootKey != WIZARD_KEY) {
needRestart = true;
@ -1286,6 +1599,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
const QJsonValue& settingValue = rootValue.toObject()[settingKey];
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY &&
rootKey != DESCRIPTION_ROOT_KEY && rootKey != WIZARD_KEY) ||
settingKey == AC_SUBNET_WHITELIST_KEY) {

View file

@ -13,6 +13,7 @@
#define hifi_DomainServerSettingsManager_h
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkReply>
@ -28,6 +29,7 @@ const QString SETTINGS_PATHS_KEY = "paths";
const QString SETTINGS_PATH = "/settings";
const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json";
const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions";
const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions";
const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions";
@ -38,6 +40,10 @@ const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens";
using GroupByUUIDKey = QPair<QUuid, QUuid>; // groupID, rankID
enum SettingsType {
DomainSettings,
ContentSettings
};
class DomainServerSettingsManager : public QObject {
Q_OBJECT
@ -123,8 +129,11 @@ private slots:
private:
QStringList _argumentList;
QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false);
bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject);
QJsonArray filteredDescriptionArray(bool isContentSettings);
QJsonObject settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated = false,
bool includeDomainSettings = true, bool includeContentSettings = true,
bool includeDefaults = true, bool isForBackup = false);
bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType);
void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap,
const QJsonObject& settingDescription);
@ -132,8 +141,17 @@ private:
void sortPermissions();
void persistToFile();
void splitSettingsDescription();
bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType);
double _descriptionVersion;
QJsonArray _descriptionArray;
QJsonArray _domainSettingsDescription;
QJsonArray _contentSettingsDescription;
QJsonObject _settingsMenuGroups;
HifiConfigVariantMap _configMap;
friend class DomainServer;

View file

@ -88,8 +88,8 @@
]
},
{ "from": "Keyboard.W", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.S", "to": "Actions.LONGITUDINAL_BACKWARD" },
{ "from": "Keyboard.W", "when": "!Keyboard.Control", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.S", "when": "!Keyboard.Control", "to": "Actions.LONGITUDINAL_BACKWARD" },
{ "from": "Keyboard.C", "to": "Actions.VERTICAL_DOWN" },
{ "from": "Keyboard.E", "to": "Actions.VERTICAL_UP" },
{ "from": "Keyboard.Left", "when": "Keyboard.RightMouseButton", "to": "Actions.LATERAL_LEFT" },

View file

@ -202,8 +202,4 @@ TreeView {
}
onDoubleClicked: isExpanded(index) ? collapse(index) : expand(index)
onClicked: {
selectionModel.setCurrentIndex(index, ItemSelectionModel.ClearAndSelect);
}
}

View file

@ -37,7 +37,7 @@ Windows.ScrollingWindow {
property var assetProxyModel: Assets.proxyModel;
property var assetMappingsModel: Assets.mappingModel;
property var currentDirectory;
property var selectedItems: treeView.selection.selectedIndexes.length;
property var selectedItemCount: treeView.selection.selectedIndexes.length;
Settings {
category: "Overlay.AssetServer"
@ -75,17 +75,17 @@ Windows.ScrollingWindow {
});
}
function doDeleteFile(path) {
console.log("Deleting " + path);
function doDeleteFile(paths) {
console.log("Deleting " + paths);
Assets.deleteMappings(path, function(err) {
Assets.deleteMappings(paths, function(err) {
if (err) {
console.log("Asset browser - error deleting path: ", path, err);
console.log("Asset browser - error deleting paths: ", paths, err);
box = errorMessageBox("There was an error deleting:\n" + path + "\n" + err);
box = errorMessageBox("There was an error deleting:\n" + paths + "\n" + err);
box.selected.connect(reload);
} else {
console.log("Asset browser - finished deleting path: ", path);
console.log("Asset browser - finished deleting paths: ", paths);
reload();
}
});
@ -145,7 +145,7 @@ Windows.ScrollingWindow {
function canAddToWorld(path) {
var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i];
if (selectedItems > 1) {
if (selectedItemCount > 1) {
return false;
}
@ -155,7 +155,7 @@ Windows.ScrollingWindow {
}
function canRename() {
if (treeView.selection.hasSelection && selectedItems == 1) {
if (treeView.selection.hasSelection && selectedItemCount == 1) {
return true;
} else {
return false;
@ -344,29 +344,28 @@ Windows.ScrollingWindow {
});
}
function deleteFile(index) {
var path = [];
var paths = [];
if (!index) {
for (var i = 0; i < selectedItems; i++) {
treeView.selection.setCurrentIndex(treeView.selection.selectedIndexes[i], 0x100);
index = treeView.selection.currentIndex;
path[i] = assetProxyModel.data(index, 0x100);
for (var i = 0; i < selectedItemCount; ++i) {
index = treeView.selection.selectedIndexes[i];
paths[i] = assetProxyModel.data(index, 0x100);
}
}
if (!path) {
if (!paths) {
return;
}
var modalMessage = "";
var items = selectedItems.toString();
var items = selectedItemCount.toString();
var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101);
var typeString = isFolder ? 'folder' : 'file';
if (selectedItems > 1) {
if (selectedItemCount > 1) {
modalMessage = "You are about to delete " + items + " items \nDo you want to continue?";
} else {
modalMessage = "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?";
modalMessage = "You are about to delete the following " + typeString + ":\n" + paths + "\nDo you want to continue?";
}
var object = desktop.messageBox({
@ -378,7 +377,7 @@ Windows.ScrollingWindow {
});
object.selected.connect(function(button) {
if (button === OriginalDialogs.StandardButton.Yes) {
doDeleteFile(path);
doDeleteFile(paths);
}
});
}
@ -705,7 +704,7 @@ Windows.ScrollingWindow {
}
}
}
}
}// End_OF( itemLoader )
Rectangle {
id: treeLabelToolTip
@ -742,50 +741,59 @@ Windows.ScrollingWindow {
showTimer.stop();
treeLabelToolTip.visible = false;
}
}
}// End_OF( treeLabelToolTip )
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (!HMD.active) { // Popup only displays properly on desktop
var index = treeView.indexAt(mouse.x, mouse.y);
treeView.selection.setCurrentIndex(index, 0x0002);
contextMenu.currentIndex = index;
contextMenu.popup();
if (treeView.selection.hasSelection && !HMD.active) { // Popup only displays properly on desktop
// Only display the popup if the click triggered within
// the selection.
var clickedIndex = treeView.indexAt(mouse.x, mouse.y);
var displayContextMenu = false;
for ( var i = 0; i < selectedItemCount; ++i) {
var currentSelectedIndex = treeView.selection.selectedIndexes[i];
if (clickedIndex === currentSelectedIndex) {
contextMenu.popup();
break;
}
}
}
}
}
Menu {
id: contextMenu
title: "Edit"
property var url: ""
property var currentIndex: null
MenuItem {
text: "Copy URL"
enabled: (selectedItemCount == 1)
onTriggered: {
copyURLToClipboard(contextMenu.currentIndex);
copyURLToClipboard(treeView.selection.currentIndex);
}
}
MenuItem {
text: "Rename"
enabled: (selectedItemCount == 1)
onTriggered: {
renameFile(contextMenu.currentIndex);
renameFile(treeView.selection.currentIndex);
}
}
MenuItem {
text: "Delete"
enabled: (selectedItemCount > 0)
onTriggered: {
deleteFile(contextMenu.currentIndex);
deleteFile();
}
}
}
}
}// End_OF( contextMenu )
}// End_OF( treeView )
Row {
id: infoRow
@ -798,8 +806,8 @@ Windows.ScrollingWindow {
function makeText() {
var numPendingBakes = assetMappingsModel.numPendingBakes;
if (selectedItems > 1 || numPendingBakes === 0) {
return selectedItems + " items selected";
if (selectedItemCount > 1 || numPendingBakes === 0) {
return selectedItemCount + " items selected";
} else {
return numPendingBakes + " bakes pending"
}
@ -896,7 +904,7 @@ Windows.ScrollingWindow {
"Baking compresses and optimizes files for faster network transfer and display. We recommend you bake your content to reduce initial load times for your visitors.");
}
}
}
}// End_OF( infoRow )
HifiControls.ContentSection {
id: uploadSection
@ -956,7 +964,7 @@ Windows.ScrollingWindow {
}
}
}
}
}// End_OF( uploadSection )
}
}

View file

@ -391,7 +391,7 @@ Item {
anchors.topMargin: 15;
width: 118;
height: paintedHeight;
wrapMode: Text.WordWrap;
wrapMode: Text.Wrap;
// Alignment
horizontalAlignment: Text.AlignRight;
}
@ -408,7 +408,7 @@ Item {
height: paintedHeight;
color: model.status === "invalidated" ? hifi.colors.redAccent : hifi.colors.baseGrayHighlight;
linkColor: hifi.colors.blueAccent;
wrapMode: Text.WordWrap;
wrapMode: Text.Wrap;
font.strikeout: model.status === "invalidated";
onLinkActivated: {

View file

@ -39,7 +39,7 @@ Rectangle {
property var assetProxyModel: Assets.proxyModel;
property var assetMappingsModel: Assets.mappingModel;
property var currentDirectory;
property var selectedItems: treeView.selection.selectedIndexes.length;
property var selectedItemCount: treeView.selection.selectedIndexes.length;
Settings {
category: "Overlay.AssetServer"
@ -76,17 +76,17 @@ Rectangle {
});
}
function doDeleteFile(path) {
console.log("Deleting " + path);
function doDeleteFile(paths) {
console.log("Deleting " + paths);
Assets.deleteMappings(path, function(err) {
Assets.deleteMappings(paths, function(err) {
if (err) {
console.log("Asset browser - error deleting path: ", path, err);
console.log("Asset browser - error deleting paths: ", paths, err);
box = errorMessageBox("There was an error deleting:\n" + path + "\n" + err);
box = errorMessageBox("There was an error deleting:\n" + paths + "\n" + err);
box.selected.connect(reload);
} else {
console.log("Asset browser - finished deleting path: ", path);
console.log("Asset browser - finished deleting paths: ", paths);
reload();
}
});
@ -146,7 +146,7 @@ Rectangle {
function canAddToWorld(path) {
var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i];
if (selectedItems > 1) {
if (selectedItemCount > 1) {
return false;
}
@ -156,7 +156,7 @@ Rectangle {
}
function canRename() {
if (treeView.selection.hasSelection && selectedItems == 1) {
if (treeView.selection.hasSelection && selectedItemCount == 1) {
return true;
} else {
return false;
@ -345,29 +345,28 @@ Rectangle {
});
}
function deleteFile(index) {
var path = [];
var paths = [];
if (!index) {
for (var i = 0; i < selectedItems; i++) {
treeView.selection.setCurrentIndex(treeView.selection.selectedIndexes[i], 0x100);
index = treeView.selection.currentIndex;
path[i] = assetProxyModel.data(index, 0x100);
for (var i = 0; i < selectedItemCount; ++i) {
index = treeView.selection.selectedIndexes[i];
paths[i] = assetProxyModel.data(index, 0x100);
}
}
if (!path) {
if (!paths) {
return;
}
var modalMessage = "";
var items = selectedItems.toString();
var items = selectedItemCount.toString();
var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101);
var typeString = isFolder ? 'folder' : 'file';
if (selectedItems > 1) {
if (selectedItemCount > 1) {
modalMessage = "You are about to delete " + items + " items \nDo you want to continue?";
} else {
modalMessage = "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?";
modalMessage = "You are about to delete the following " + typeString + ":\n" + paths + "\nDo you want to continue?";
}
var object = tabletRoot.messageBox({
@ -379,7 +378,7 @@ Rectangle {
});
object.selected.connect(function(button) {
if (button === OriginalDialogs.StandardButton.Yes) {
doDeleteFile(path);
doDeleteFile(paths);
}
});
}
@ -704,7 +703,7 @@ Rectangle {
}
}
}
}
}// End_OF( itemLoader )
Rectangle {
id: treeLabelToolTip
@ -741,50 +740,59 @@ Rectangle {
showTimer.stop();
treeLabelToolTip.visible = false;
}
}
}// End_OF( treeLabelToolTip )
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (!HMD.active) { // Popup only displays properly on desktop
var index = treeView.indexAt(mouse.x, mouse.y);
treeView.selection.setCurrentIndex(index, 0x0002);
contextMenu.currentIndex = index;
contextMenu.popup();
if (treeView.selection.hasSelection && !HMD.active) { // Popup only displays properly on desktop
// Only display the popup if the click triggered within
// the selection.
var clickedIndex = treeView.indexAt(mouse.x, mouse.y);
var displayContextMenu = false;
for ( var i = 0; i < selectedItemCount; ++i) {
var currentSelectedIndex = treeView.selection.selectedIndexes[i];
if (clickedIndex === currentSelectedIndex) {
contextMenu.popup();
break;
}
}
}
}
}
Menu {
id: contextMenu
title: "Edit"
property var url: ""
property var currentIndex: null
MenuItem {
text: "Copy URL"
enabled: (selectedItemCount == 1)
onTriggered: {
copyURLToClipboard(contextMenu.currentIndex);
copyURLToClipboard(treeView.selection.currentIndex);
}
}
MenuItem {
text: "Rename"
enabled: (selectedItemCount == 1)
onTriggered: {
renameFile(contextMenu.currentIndex);
renameFile(treeView.selection.currentIndex);
}
}
MenuItem {
text: "Delete"
enabled: (selectedItemCount > 0)
onTriggered: {
deleteFile(contextMenu.currentIndex);
deleteFile();
}
}
}
}
}// End_OF( contextMenu )
}// End_OF( treeView )
Row {
id: infoRow
@ -797,8 +805,8 @@ Rectangle {
function makeText() {
var numPendingBakes = assetMappingsModel.numPendingBakes;
if (selectedItems > 1 || numPendingBakes === 0) {
return selectedItems + " items selected";
if (selectedItemCount > 1 || numPendingBakes === 0) {
return selectedItemCount + " items selected";
} else {
return numPendingBakes + " bakes pending"
}
@ -895,7 +903,7 @@ Rectangle {
"Baking compresses and optimizes files for faster network transfer and display. We recommend you bake your content to reduce initial load times for your visitors.");
}
}
}
}// End_OF( infoRow )
HifiControls.TabletContentSection {
id: uploadSection
@ -972,7 +980,7 @@ Rectangle {
}
}
}
}
}// End_OF( uploadSection )
}
}

Binary file not shown.

View file

@ -2405,6 +2405,7 @@ void Application::initializeGL() {
qFatal("Unable to make offscreen context current");
}
_offscreenContext->doneCurrent();
_offscreenContext->setThreadContext();
_renderEventHandler = new RenderEventHandler(_glWidget->qglContext());
// The UI can't be created until the primary OpenGL

View file

@ -1111,7 +1111,7 @@ void MyAvatar::setEnableDebugDrawIKChains(bool isEnabled) {
}
void MyAvatar::setEnableMeshVisible(bool isEnabled) {
_skeletonModel->setVisibleInScene(isEnabled, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE);
_skeletonModel->setVisibleInScene(isEnabled, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE, true);
}
void MyAvatar::setEnableInverseKinematics(bool isEnabled) {
@ -1460,10 +1460,23 @@ void MyAvatar::clearJointsData() {
}
void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
_skeletonModelChangeCount++;
int skeletonModelChangeCount = _skeletonModelChangeCount;
Avatar::setSkeletonModelURL(skeletonModelURL);
_skeletonModel->setVisibleInScene(true, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE);
_skeletonModel->setVisibleInScene(true, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE, true);
_headBoneSet.clear();
_cauterizationNeedsUpdate = true;
std::shared_ptr<QMetaObject::Connection> skeletonConnection = std::make_shared<QMetaObject::Connection>();
*skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, [this, skeletonModelChangeCount, skeletonConnection]() {
if (skeletonModelChangeCount == _skeletonModelChangeCount) {
initHeadBones();
_skeletonModel->setCauterizeBoneSet(_headBoneSet);
_fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl();
initAnimGraph();
}
QObject::disconnect(*skeletonConnection);
});
saveAvatarUrl();
emit skeletonChanged();
@ -1808,7 +1821,7 @@ void MyAvatar::attach(const QString& modelURL, const QString& jointName,
void MyAvatar::setVisibleInSceneIfReady(Model* model, const render::ScenePointer& scene, bool visible) {
if (model->isActive() && model->isRenderable()) {
model->setVisibleInScene(visible, scene, render::ItemKey::TAG_BITS_NONE);
model->setVisibleInScene(visible, scene, render::ItemKey::TAG_BITS_NONE, true);
}
}
@ -1872,9 +1885,7 @@ void MyAvatar::setAnimGraphUrl(const QUrl& url) {
_currentAnimGraphUrl.set(url);
_skeletonModel->getRig().initAnimGraph(url);
_bodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation..
updateSensorToWorldMatrix(); // Uses updated position/orientation and _bodySensorMatrix changes
connect(&(_skeletonModel->getRig()), SIGNAL(onLoadComplete()), this, SLOT(animGraphLoaded()));
}
void MyAvatar::initAnimGraph() {
@ -1889,28 +1900,24 @@ void MyAvatar::initAnimGraph() {
_skeletonModel->getRig().initAnimGraph(graphUrl);
_currentAnimGraphUrl.set(graphUrl);
_bodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation..
updateSensorToWorldMatrix(); // Uses updated position/orientation and _bodySensorMatrix changes
connect(&(_skeletonModel->getRig()), SIGNAL(onLoadComplete()), this, SLOT(animGraphLoaded()));
}
void MyAvatar::destroyAnimGraph() {
_skeletonModel->getRig().destroyAnimGraph();
}
void MyAvatar::animGraphLoaded() {
_bodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation..
updateSensorToWorldMatrix(); // Uses updated position/orientation and _bodySensorMatrix changes
_isAnimatingScale = true;
_cauterizationNeedsUpdate = true;
disconnect(&(_skeletonModel->getRig()), SIGNAL(onLoadComplete()), this, SLOT(animGraphLoaded()));
}
void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) {
Avatar::postUpdate(deltaTime, scene);
if (_skeletonModel->isLoaded() && !_skeletonModel->getRig().getAnimNode()) {
initHeadBones();
_skeletonModel->setCauterizeBoneSet(_headBoneSet);
_fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl();
initAnimGraph();
_isAnimatingScale = true;
_cauterizationNeedsUpdate = true;
}
if (_enableDebugDrawDefaultPose || _enableDebugDrawAnimPose) {
auto animSkeleton = _skeletonModel->getRig().getAnimSkeleton();
@ -2008,7 +2015,7 @@ void MyAvatar::preDisplaySide(RenderArgs* renderArgs) {
_attachmentData[i].jointName.compare("HeadTop_End", Qt::CaseInsensitive) == 0 ||
_attachmentData[i].jointName.compare("Face", Qt::CaseInsensitive) == 0) {
_attachmentModels[i]->setVisibleInScene(shouldDrawHead, qApp->getMain3DScene(),
render::ItemKey::TAG_BITS_NONE);
render::ItemKey::TAG_BITS_NONE, true);
}
}
}

View file

@ -569,6 +569,7 @@ public slots:
void increaseSize();
void decreaseSize();
void resetSize();
void animGraphLoaded();
void setGravity(float gravity);
float getGravity();
@ -654,6 +655,7 @@ private:
bool isMyAvatar() const override { return true; }
virtual int parseDataFromBuffer(const QByteArray& buffer) override;
virtual glm::vec3 getSkeletonPosition() const override;
int _skeletonModelChangeCount { 0 };
void saveAvatarScale();

View file

@ -425,10 +425,10 @@ void Circle3DOverlay::setProperties(const QVariantMap& properties) {
* <em>Write-only.</em>
* @property {Color} outerColor - Sets the values of <code>outerStartColor</code> and <code>outerEndColor</code>.
* <em>Write-only.</em>
* @property {Color} innerStartcolor - The color at the inner start point of the overlay. <em>Write-only.</em>
* @property {Color} innerEndColor - The color at the inner end point of the overlay. <em>Write-only.</em>
* @property {Color} outerStartColor - The color at the outer start point of the overlay. <em>Write-only.</em>
* @property {Color} outerEndColor - The color at the outer end point of the overlay. <em>Write-only.</em>
* @property {Color} innerStartcolor - The color at the inner start point of the overlay.
* @property {Color} innerEndColor - The color at the inner end point of the overlay.
* @property {Color} outerStartColor - The color at the outer start point of the overlay.
* @property {Color} outerEndColor - The color at the outer end point of the overlay.
* @property {number} alpha=0.5 - The opacity of the overlay, <code>0.0</code> - <code>1.0</code>. Setting this value also sets
* the values of <code>innerStartAlpha</code>, <code>innerEndAlpha</code>, <code>outerStartAlpha</code>, and
* <code>outerEndAlpha</code>. Synonym: <code>Alpha</code>; <em>write-only</em>.
@ -440,10 +440,10 @@ void Circle3DOverlay::setProperties(const QVariantMap& properties) {
* <em>Write-only.</em>
* @property {number} outerAlpha - Sets the values of <code>outerStartAlpha</code> and <code>outerEndAlpha</code>.
* <em>Write-only.</em>
* @property {number} innerStartAlpha=0 - The alpha at the inner start point of the overlay. <em>Write-only.</em>
* @property {number} innerEndAlpha=0 - The alpha at the inner end point of the overlay. <em>Write-only.</em>
* @property {number} outerStartAlpha=0 - The alpha at the outer start point of the overlay. <em>Write-only.</em>
* @property {number} outerEndAlpha=0 - The alpha at the outer end point of the overlay. <em>Write-only.</em>
* @property {number} innerStartAlpha=0 - The alpha at the inner start point of the overlay.
* @property {number} innerEndAlpha=0 - The alpha at the inner end point of the overlay.
* @property {number} outerStartAlpha=0 - The alpha at the outer start point of the overlay.
* @property {number} outerEndAlpha=0 - The alpha at the outer end point of the overlay.
* @property {boolean} hasTickMarks=false - If <code>true</code>, tick marks are drawn.
* @property {number} majorTickMarksAngle=0 - The angle between major tick marks, in degrees.

View file

@ -88,7 +88,7 @@ void ModelOverlay::update(float deltatime) {
if (_visibleDirty) {
_visibleDirty = false;
// don't show overlays in mirrors
_model->setVisibleInScene(getVisible(), scene, render::ItemKey::TAG_BITS_0);
_model->setVisibleInScene(getVisible(), scene, render::ItemKey::TAG_BITS_0, false);
}
if (_drawInFrontDirty) {
_drawInFrontDirty = false;

View file

@ -38,6 +38,8 @@ static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const Q
static AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadDefaultPoseNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static const float ANIM_GRAPH_LOAD_PRIORITY = 10.0f;
// called after children have been loaded
// returns node on success, nullptr on failure.
static bool processDoNothing(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; }
@ -653,6 +655,7 @@ AnimNodeLoader::AnimNodeLoader(const QUrl& url) :
{
_resource = QSharedPointer<Resource>::create(url);
_resource->setSelf(_resource);
_resource->setLoadPriority(this, ANIM_GRAPH_LOAD_PRIORITY);
connect(_resource.data(), &Resource::loaded, this, &AnimNodeLoader::onRequestDone);
connect(_resource.data(), &Resource::failed, this, &AnimNodeLoader::onRequestError);
_resource->ensureLoading();

View file

@ -1585,14 +1585,13 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo
}
void Rig::initAnimGraph(const QUrl& url) {
if (_animGraphURL != url || (!_animNode && !_animLoading)) {
if (_animGraphURL != url || !_animNode) {
_animGraphURL = url;
_animNode.reset();
// load the anim graph
_animLoader.reset(new AnimNodeLoader(url));
_animLoading = true;
std::weak_ptr<AnimSkeleton> weakSkeletonPtr = _animSkeleton;
connect(_animLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr](AnimNode::Pointer nodeIn) {
_animNode = nodeIn;
@ -1617,7 +1616,6 @@ void Rig::initAnimGraph(const QUrl& url) {
auto roleState = roleAnimState.second;
overrideRoleAnimation(roleState.role, roleState.url, roleState.fps, roleState.loop, roleState.firstFrame, roleState.lastFrame);
}
_animLoading = false;
emit onLoadComplete();
});

View file

@ -283,7 +283,6 @@ protected:
std::shared_ptr<AnimNode> _animNode;
std::shared_ptr<AnimSkeleton> _animSkeleton;
std::unique_ptr<AnimNodeLoader> _animLoader;
bool _animLoading { false };
AnimVariantMap _animVars;
enum class RigRole {
Idle = 0,

View file

@ -50,7 +50,7 @@ const glm::vec3 HAND_TO_PALM_OFFSET(0.0f, 0.12f, 0.08f);
namespace render {
template <> const ItemKey payloadGetKey(const AvatarSharedPointer& avatar) {
return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(ItemKey::TAG_BITS_0 | ItemKey::TAG_BITS_1);
return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(ItemKey::TAG_BITS_0 | ItemKey::TAG_BITS_1).withMetaCullGroup();
}
template <> const Item::Bound payloadGetBound(const AvatarSharedPointer& avatar) {
return static_pointer_cast<Avatar>(avatar)->getBounds();

View file

@ -30,6 +30,20 @@ QVector<QUuid> AvatarHashMap::getAvatarIdentifiers() {
return _avatarHash.keys().toVector();
}
QVector<QUuid> AvatarHashMap::getAvatarsInRange(const glm::vec3& position, float rangeMeters) const {
auto hashCopy = getHashCopy();
QVector<QUuid> avatarsInRange;
auto rangeMetersSquared = rangeMeters * rangeMeters;
for (const AvatarSharedPointer& sharedAvatar : hashCopy) {
glm::vec3 avatarPosition = sharedAvatar->getWorldPosition();
auto distanceSquared = glm::distance2(avatarPosition, position);
if (distanceSquared < rangeMetersSquared) {
avatarsInRange.push_back(sharedAvatar->getSessionUUID());
}
}
return avatarsInRange;
}
bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range) {
auto hashCopy = getHashCopy();
foreach(const AvatarSharedPointer& sharedAvatar, hashCopy) {

View file

@ -35,10 +35,12 @@ class AvatarHashMap : public QObject, public Dependency {
public:
AvatarHash getHashCopy() { QReadLocker lock(&_hashLock); return _avatarHash; }
const AvatarHash getHashCopy() const { QReadLocker lock(&_hashLock); return _avatarHash; }
int size() { return _avatarHash.size(); }
// Currently, your own avatar will be included as the null avatar id.
Q_INVOKABLE QVector<QUuid> getAvatarIdentifiers();
Q_INVOKABLE QVector<QUuid> getAvatarsInRange(const glm::vec3& position, float rangeMeters) const;
// Null/Default-constructed QUuids will return MyAvatar
Q_INVOKABLE virtual ScriptAvatarData* getAvatar(QUuid avatarID) { return new ScriptAvatarData(getAvatarBySessionID(avatarID)); }

View file

@ -27,6 +27,7 @@
#include <gl/GLWidget.h>
#include <gl/GLEscrow.h>
#include <gl/Context.h>
#include <gl/OffscreenGLCanvas.h>
#include <gpu/Texture.h>
#include <gpu/StandardShaderLib.h>
@ -130,14 +131,14 @@ public:
CHECK_GL_ERROR();
_context->doneCurrent();
while (!_shutdown) {
if (_pendingMainThreadOperation) {
if (_pendingOtherThreadOperation) {
PROFILE_RANGE(render, "MainThreadOp")
{
Lock lock(_mutex);
_context->doneCurrent();
// Move the context to the main thread
_context->moveToThread(qApp->thread());
_pendingMainThreadOperation = false;
_context->moveToThread(_targetOperationThread);
_pendingOtherThreadOperation = false;
// Release the main thread to do it's action
_condition.notify_one();
}
@ -146,7 +147,7 @@ public:
{
// Main thread does it's thing while we wait on the lock to release
Lock lock(_mutex);
_condition.wait(lock, [&] { return _finishedMainThreadOperation; });
_condition.wait(lock, [&] { return _finishedOtherThreadOperation; });
}
}
@ -214,23 +215,25 @@ public:
_condition.notify_one();
}
void withMainThreadContext(std::function<void()> f) {
void withOtherThreadContext(std::function<void()> f) {
// Signal to the thread that there is work to be done on the main thread
Lock lock(_mutex);
_pendingMainThreadOperation = true;
_finishedMainThreadOperation = false;
_condition.wait(lock, [&] { return !_pendingMainThreadOperation; });
_targetOperationThread = QThread::currentThread();
_pendingOtherThreadOperation = true;
_finishedOtherThreadOperation = false;
_condition.wait(lock, [&] { return !_pendingOtherThreadOperation; });
_context->makeCurrent();
f();
_context->doneCurrent();
_targetOperationThread = nullptr;
// Move the context back to the presentation thread
_context->moveToThread(this);
// restore control of the context to the presentation thread and signal
// the end of the operation
_finishedMainThreadOperation = true;
_finishedOtherThreadOperation = true;
lock.unlock();
_condition.notify_one();
}
@ -244,9 +247,11 @@ private:
Mutex _mutex;
// Used to allow the main thread to perform context operations
Condition _condition;
bool _pendingMainThreadOperation { false };
bool _finishedMainThreadOperation { false };
QThread* _mainThread { nullptr };
QThread* _targetOperationThread { nullptr };
bool _pendingOtherThreadOperation { false };
bool _finishedOtherThreadOperation { false };
std::queue<OpenGLDisplayPlugin*> _newPluginQueue;
gl::Context* _context { nullptr };
};
@ -744,10 +749,12 @@ void OpenGLDisplayPlugin::swapBuffers() {
context->swapBuffers();
}
void OpenGLDisplayPlugin::withMainThreadContext(std::function<void()> f) const {
void OpenGLDisplayPlugin::withOtherThreadContext(std::function<void()> f) const {
static auto presentThread = DependencyManager::get<PresentThread>();
presentThread->withMainThreadContext(f);
_container->makeRenderingContextCurrent();
presentThread->withOtherThreadContext(f);
if (!OffscreenGLCanvas::restoreThreadContext()) {
qWarning("Unable to restore original OpenGL context");
}
}
bool OpenGLDisplayPlugin::setDisplayTexture(const QString& name) {
@ -784,7 +791,7 @@ QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const {
}
auto glBackend = const_cast<OpenGLDisplayPlugin&>(*this).getGLBackend();
QImage screenshot(bestSize.x, bestSize.y, QImage::Format_ARGB32);
withMainThreadContext([&] {
withOtherThreadContext([&] {
glBackend->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot);
});
return screenshot.mirrored(false, true);
@ -797,7 +804,7 @@ QImage OpenGLDisplayPlugin::getSecondaryCameraScreenshot() const {
auto glBackend = const_cast<OpenGLDisplayPlugin&>(*this).getGLBackend();
QImage screenshot(region.z, region.w, QImage::Format_ARGB32);
withMainThreadContext([&] {
withOtherThreadContext([&] {
glBackend->downloadFramebuffer(secondaryCameraFramebuffer, region, screenshot);
});
return screenshot.mirrored(false, true);
@ -886,7 +893,7 @@ void OpenGLDisplayPlugin::updateCompositeFramebuffer() {
void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer networkTexture, QOpenGLFramebufferObject* target, GLsync* fenceSync) {
#if !defined(USE_GLES)
auto glBackend = const_cast<OpenGLDisplayPlugin&>(*this).getGLBackend();
withMainThreadContext([&] {
withOtherThreadContext([&] {
GLuint sourceTexture = glBackend->getTextureID(networkTexture->getGPUTexture());
GLuint targetTexture = target->texture();
GLuint fbo[2] {0, 0};

View file

@ -119,7 +119,7 @@ protected:
void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor);
virtual void updateFrameData();
void withMainThreadContext(std::function<void()> f) const;
void withOtherThreadContext(std::function<void()> f) const;
void present();
virtual void swapBuffers();

View file

@ -79,7 +79,7 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url,
QHash<QByteArray, QByteArray> redirectHeader;
redirectHeader.insert(QByteArray("Location"), redirectLocation.toUtf8());
connection->respond(HTTPConnection::StatusCode301, "", HTTPConnection::DefaultContentType, redirectHeader);
connection->respond(HTTPConnection::StatusCode302, "", HTTPConnection::DefaultContentType, redirectHeader);
}
// if the last thing is a trailing slash then we want to look for index file

View file

@ -985,6 +985,7 @@ void RenderableModelEntityItem::copyAnimationJointDataToModel() {
return;
}
bool changed { false };
// relay any inbound joint changes from scripts/animation/network to the model/rig
_jointDataLock.withWriteLock([&] {
for (int index = 0; index < _localJointData.size(); ++index) {
@ -992,13 +993,21 @@ void RenderableModelEntityItem::copyAnimationJointDataToModel() {
if (jointData.rotationDirty) {
model->setJointRotation(index, true, jointData.joint.rotation, 1.0f);
jointData.rotationDirty = false;
changed = true;
}
if (jointData.translationDirty) {
model->setJointTranslation(index, true, jointData.joint.translation, 1.0f);
jointData.translationDirty = false;
changed = true;
}
}
});
if (changed) {
forEachChild([&](SpatiallyNestablePointer object) {
object->locationChanged(false);
});
}
}
using namespace render;
@ -1343,7 +1352,7 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
// FIXME: this seems like it could be optimized if we tracked our last known visible state in
// the renderable item. As it stands now the model checks it's visible/invisible state
// so most of the time we don't do anything in this function.
model->setVisibleInScene(_visible, scene, viewTaskBits);
model->setVisibleInScene(_visible, scene, viewTaskBits, false);
}
// TODO? early exit here when not visible?

View file

@ -16,6 +16,7 @@
#include <QtCore/QProcessEnvironment>
#include <QtCore/QDebug>
#include <QtCore/QThread>
#include <QtGui/QOffscreenSurface>
#include <QtGui/QOpenGLContext>
#include <QtGui/QOpenGLDebugLogger>
@ -119,3 +120,29 @@ void OffscreenGLCanvas::moveToThreadWithContext(QThread* thread) {
moveToThread(thread);
_context->moveToThread(thread);
}
static const char* THREAD_CONTEXT_PROPERTY = "offscreenGlCanvas";
void OffscreenGLCanvas::setThreadContext() {
QThread::currentThread()->setProperty(THREAD_CONTEXT_PROPERTY, QVariant::fromValue<QObject*>(this));
}
bool OffscreenGLCanvas::restoreThreadContext() {
// Restore the rendering context for this thread
auto threadCanvasVariant = QThread::currentThread()->property(THREAD_CONTEXT_PROPERTY);
if (!threadCanvasVariant.isValid()) {
return false;
}
auto threadCanvasObject = qvariant_cast<QObject*>(threadCanvasVariant);
auto threadCanvas = static_cast<OffscreenGLCanvas*>(threadCanvasObject);
if (!threadCanvas) {
return false;
}
if (!threadCanvas->makeCurrent()) {
qFatal("Unable to restore Offscreen rendering context");
}
return true;
}

View file

@ -32,6 +32,9 @@ public:
}
QObject* getContextObject();
void setThreadContext();
static bool restoreThreadContext();
private slots:
void onMessageLogged(const QOpenGLDebugMessage &debugMessage);

View file

@ -54,11 +54,9 @@ void KeyboardMouseDevice::InputDevice::focusOutEvent() {
void KeyboardMouseDevice::keyPressEvent(QKeyEvent* event) {
auto input = _inputDevice->makeInput((Qt::Key) event->key());
if (!(event->modifiers() & Qt::KeyboardModifier::ControlModifier)) {
auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel());
if (result.second) {
// key pressed again ? without catching the release event ?
}
auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel());
if (result.second) {
// key pressed again ? without catching the release event ?
}
}
@ -237,6 +235,7 @@ controller::Input::NamedVector KeyboardMouseDevice::InputDevice::getAvailableInp
availableInputs.append(Input::NamedPair(makeInput(Qt::Key_PageUp), QKeySequence(Qt::Key_PageUp).toString()));
availableInputs.append(Input::NamedPair(makeInput(Qt::Key_PageDown), QKeySequence(Qt::Key_PageDown).toString()));
availableInputs.append(Input::NamedPair(makeInput(Qt::Key_Tab), QKeySequence(Qt::Key_Tab).toString()));
availableInputs.append(Input::NamedPair(makeInput(Qt::Key_Control), "Control"));
availableInputs.append(Input::NamedPair(makeInput(Qt::LeftButton), "LeftMouseButton"));
availableInputs.append(Input::NamedPair(makeInput(Qt::MiddleButton), "MiddleMouseButton"));

View file

@ -40,7 +40,7 @@ AssetClient::AssetClient() {
static_cast<AssetClient*>(dependency)->deleteLater();
});
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
auto& packetReceiver = nodeList->getPacketReceiver();
packetReceiver.registerListener(PacketType::AssetMappingOperationReply, this, "handleAssetMappingOperationReply");
@ -308,7 +308,7 @@ void AssetClient::handleAssetMappingOperationReply(QSharedPointer<ReceivedMessag
}
bool haveAssetServer() {
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (!assetServer) {
@ -402,7 +402,7 @@ MessageID AssetClient::getAsset(const QString& hash, AssetUtils::DataOffset star
return false;
}
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
@ -435,7 +435,7 @@ MessageID AssetClient::getAsset(const QString& hash, AssetUtils::DataOffset star
MessageID AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callback) {
Q_ASSERT(QThread::currentThread() == thread());
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
@ -635,7 +635,7 @@ void AssetClient::handleCompleteCallback(const QWeakPointer<Node>& node, Message
MessageID AssetClient::getAssetMapping(const AssetUtils::AssetPath& path, MappingOperationCallback callback) {
Q_ASSERT(QThread::currentThread() == thread());
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
@ -662,7 +662,7 @@ MessageID AssetClient::getAssetMapping(const AssetUtils::AssetPath& path, Mappin
MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) {
Q_ASSERT(QThread::currentThread() == thread());
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
@ -685,7 +685,7 @@ MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) {
}
MessageID AssetClient::deleteAssetMappings(const AssetUtils::AssetPathList& paths, MappingOperationCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
@ -716,7 +716,7 @@ MessageID AssetClient::deleteAssetMappings(const AssetUtils::AssetPathList& path
MessageID AssetClient::setAssetMapping(const QString& path, const AssetUtils::AssetHash& hash, MappingOperationCallback callback) {
Q_ASSERT(QThread::currentThread() == thread());
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
@ -742,7 +742,7 @@ MessageID AssetClient::setAssetMapping(const QString& path, const AssetUtils::As
}
MessageID AssetClient::renameAssetMapping(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath, MappingOperationCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
@ -769,7 +769,7 @@ MessageID AssetClient::renameAssetMapping(const AssetUtils::AssetPath& oldPath,
}
MessageID AssetClient::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool enabled, MappingOperationCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
@ -859,7 +859,7 @@ bool AssetClient::cancelUploadAssetRequest(MessageID id) {
MessageID AssetClient::uploadAsset(const QByteArray& data, UploadResultCallback callback) {
Q_ASSERT(QThread::currentThread() == thread());
auto nodeList = DependencyManager::get<NodeList>();
auto nodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {

View file

@ -44,7 +44,9 @@ enum AssetServerError : uint8_t {
AssetTooLarge,
PermissionDenied,
MappingOperationFailed,
FileOperationFailed
FileOperationFailed,
NoAssetServer,
LostConnection
};
enum AssetMappingOperationType : uint8_t {
@ -71,7 +73,8 @@ struct MappingInfo {
QString bakingErrors;
};
using AssetMapping = std::map<AssetPath, MappingInfo>;
using AssetMappings = std::map<AssetPath, MappingInfo>;
using Mappings = std::map<AssetPath, AssetHash>;
QUrl getATPUrl(const QString& input);
AssetHash extractAssetHash(const QString& input);

View file

@ -49,7 +49,7 @@ public:
const QHostAddress& getIP() const { return _sockAddr.getAddress(); }
void setIPToLocalhost() { _sockAddr.setAddress(QHostAddress(QHostAddress::LocalHost)); }
const HifiSockAddr& getSockAddr() { return _sockAddr; }
const HifiSockAddr& getSockAddr() const { return _sockAddr; }
void setSockAddr(const HifiSockAddr& sockAddr, const QString& hostname);
unsigned short getPort() const { return _sockAddr.getPort(); }

View file

@ -90,21 +90,16 @@ LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) :
updateLocalSocket();
// set &PacketReceiver::handleVerifiedPacket as the verified packet callback for the udt::Socket
_nodeSocket.setPacketHandler(
[this](std::unique_ptr<udt::Packet> packet) {
_nodeSocket.setPacketHandler([this](std::unique_ptr<udt::Packet> packet) {
_packetReceiver->handleVerifiedPacket(std::move(packet));
}
);
_nodeSocket.setMessageHandler(
[this](std::unique_ptr<udt::Packet> packet) {
});
_nodeSocket.setMessageHandler([this](std::unique_ptr<udt::Packet> packet) {
_packetReceiver->handleVerifiedMessagePacket(std::move(packet));
}
);
_nodeSocket.setMessageFailureHandler(
[this](HifiSockAddr from, udt::Packet::MessageNumber messageNumber) {
});
_nodeSocket.setMessageFailureHandler([this](HifiSockAddr from,
udt::Packet::MessageNumber messageNumber) {
_packetReceiver->handleMessageFailure(from, messageNumber);
}
);
});
// set our isPacketVerified method as the verify operator for the udt::Socket
using std::placeholders::_1;
@ -309,8 +304,19 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe
sourceNode = matchingNode.data();
}
if (!sourceNode &&
sourceID == getDomainUUID() &&
packet.getSenderSockAddr() == getDomainSockAddr() &&
PacketTypeEnum::getDomainSourcedPackets().contains(headerType)) {
// This is a packet sourced by the domain server
emit dataReceived(NodeType::Unassigned, packet.getPayloadSize());
return true;
}
if (sourceNode) {
if (!PacketTypeEnum::getNonVerifiedPackets().contains(headerType)) {
if (!PacketTypeEnum::getNonVerifiedPackets().contains(headerType) &&
!isDomainServer()) {
QByteArray packetHeaderHash = NLPacket::verificationHashInHeader(packet);
QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, sourceNode->getConnectionSecret());

View file

@ -124,6 +124,10 @@ public:
PacketReceiver& getPacketReceiver() { return *_packetReceiver; }
virtual bool isDomainServer() const { return true; }
virtual QUuid getDomainUUID() const { assert(false); return QUuid(); }
virtual HifiSockAddr getDomainSockAddr() const { assert(false); return HifiSockAddr(); }
// use sendUnreliablePacket to send an unrelaible packet (that you do not need to move)
// either to a node (via its active socket) or to a manual sockaddr
qint64 sendUnreliablePacket(const NLPacket& packet, const Node& destinationNode);

View file

@ -106,9 +106,6 @@ void GetMappingRequest::doStart() {
});
};
GetAllMappingsRequest::GetAllMappingsRequest() {
};
void GetAllMappingsRequest::doStart() {
auto assetClient = DependencyManager::get<AssetClient>();
_mappingRequestID = assetClient->getAllAssetMappings(

View file

@ -120,17 +120,15 @@ private:
class GetAllMappingsRequest : public MappingRequest {
Q_OBJECT
public:
GetAllMappingsRequest();
AssetUtils::AssetMapping getMappings() const { return _mappings; }
AssetUtils::AssetMappings getMappings() const { return _mappings; }
signals:
void finished(GetAllMappingsRequest* thisRequest);
private:
virtual void doStart() override;
AssetUtils::AssetMapping _mappings;
AssetUtils::AssetMappings _mappings;
};
class SetBakingEnabledRequest : public MappingRequest {

View file

@ -92,6 +92,10 @@ public:
void removeFromIgnoreMuteSets(const QUuid& nodeID);
virtual bool isDomainServer() const override { return false; }
virtual QUuid getDomainUUID() const override { return _domainHandler.getUUID(); }
virtual HifiSockAddr getDomainSockAddr() const override { return _domainHandler.getSockAddr(); }
public slots:
void reset(bool skipDomainHandlerReset = false);
void resetFromDomainHandler() { reset(true); }

View file

@ -267,10 +267,7 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer<ReceivedMessage> recei
QMutexLocker packetListenerLocker(&_packetListenerLock);
bool listenerIsDead = false;
auto it = _messageListenerMap.find(receivedMessage->getType());
if (it != _messageListenerMap.end() && it->method.isValid()) {
auto listener = it.value();
@ -278,82 +275,61 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer<ReceivedMessage> recei
if ((listener.deliverPending && !justReceived) || (!listener.deliverPending && !receivedMessage->isComplete())) {
return;
}
if (listener.object) {
bool success = false;
bool success = false;
Qt::ConnectionType connectionType;
// check if this is a directly connected listener
{
QMutexLocker directConnectLocker(&_directConnectSetMutex);
connectionType = _directlyConnectedObjects.contains(listener.object) ? Qt::DirectConnection : Qt::AutoConnection;
}
PacketType packetType = receivedMessage->getType();
if (matchingNode) {
matchingNode->recordBytesReceived(receivedMessage->getSize());
QMetaMethod metaMethod = listener.method;
static const QByteArray QSHAREDPOINTER_NODE_NORMALIZED = QMetaObject::normalizedType("QSharedPointer<Node>");
static const QByteArray SHARED_NODE_NORMALIZED = QMetaObject::normalizedType("SharedNodePointer");
// one final check on the QPointer before we go to invoke
if (listener.object) {
if (metaMethod.parameterTypes().contains(SHARED_NODE_NORMALIZED)) {
success = metaMethod.invoke(listener.object,
connectionType,
Q_ARG(QSharedPointer<ReceivedMessage>, receivedMessage),
Q_ARG(SharedNodePointer, matchingNode));
} else if (metaMethod.parameterTypes().contains(QSHAREDPOINTER_NODE_NORMALIZED)) {
success = metaMethod.invoke(listener.object,
connectionType,
Q_ARG(QSharedPointer<ReceivedMessage>, receivedMessage),
Q_ARG(QSharedPointer<Node>, matchingNode));
} else {
success = metaMethod.invoke(listener.object,
connectionType,
Q_ARG(QSharedPointer<ReceivedMessage>, receivedMessage));
}
} else {
listenerIsDead = true;
}
} else {
// one final check on the QPointer before we invoke
if (listener.object) {
success = listener.method.invoke(listener.object,
Q_ARG(QSharedPointer<ReceivedMessage>, receivedMessage));
} else {
listenerIsDead = true;
}
}
if (!success) {
qCDebug(networking).nospace() << "Error delivering packet " << packetType << " to listener "
<< listener.object << "::" << qPrintable(listener.method.methodSignature());
}
} else {
listenerIsDead = true;
Qt::ConnectionType connectionType;
// check if this is a directly connected listener
{
QMutexLocker directConnectLocker(&_directConnectSetMutex);
connectionType = _directlyConnectedObjects.contains(listener.object) ? Qt::DirectConnection : Qt::AutoConnection;
}
if (listenerIsDead) {
if (matchingNode) {
matchingNode->recordBytesReceived(receivedMessage->getSize());
}
QMetaMethod metaMethod = listener.method;
static const QByteArray QSHAREDPOINTER_NODE_NORMALIZED = QMetaObject::normalizedType("QSharedPointer<Node>");
static const QByteArray SHARED_NODE_NORMALIZED = QMetaObject::normalizedType("SharedNodePointer");
// one final check on the QPointer before we go to invoke
if (listener.object) {
if (metaMethod.parameterTypes().contains(SHARED_NODE_NORMALIZED)) {
success = metaMethod.invoke(listener.object,
connectionType,
Q_ARG(QSharedPointer<ReceivedMessage>, receivedMessage),
Q_ARG(SharedNodePointer, matchingNode));
} else if (metaMethod.parameterTypes().contains(QSHAREDPOINTER_NODE_NORMALIZED)) {
success = metaMethod.invoke(listener.object,
connectionType,
Q_ARG(QSharedPointer<ReceivedMessage>, receivedMessage),
Q_ARG(QSharedPointer<Node>, matchingNode));
} else {
success = metaMethod.invoke(listener.object,
connectionType,
Q_ARG(QSharedPointer<ReceivedMessage>, receivedMessage));
}
} else {
qCDebug(networking).nospace() << "Listener for packet " << receivedMessage->getType()
<< " has been destroyed. Removing from listener map.";
it = _messageListenerMap.erase(it);
// if it exists, remove the listener from _directlyConnectedObjects
{
QMutexLocker directConnectLocker(&_directConnectSetMutex);
_directlyConnectedObjects.remove(listener.object);
}
}
if (!success) {
qCDebug(networking).nospace() << "Error delivering packet " << receivedMessage->getType() << " to listener "
<< listener.object << "::" << qPrintable(listener.method.methodSignature());
}
} else if (it == _messageListenerMap.end()) {
qCWarning(networking) << "No listener found for packet type" << receivedMessage->getType();

View file

@ -177,6 +177,17 @@ public:
<< PacketTypeEnum::Value::ReplicatedKillAvatar << PacketTypeEnum::Value::ReplicatedBulkAvatarData;
return NON_SOURCED_PACKETS;
}
const static QSet<PacketTypeEnum::Value> getDomainSourcedPackets() {
const static QSet<PacketTypeEnum::Value> DOMAIN_SOURCED_PACKETS = QSet<PacketTypeEnum::Value>()
<< PacketTypeEnum::Value::AssetMappingOperation
<< PacketTypeEnum::Value::AssetMappingOperationReply
<< PacketTypeEnum::Value::AssetGet
<< PacketTypeEnum::Value::AssetGetReply
<< PacketTypeEnum::Value::AssetUpload
<< PacketTypeEnum::Value::AssetUploadReply;
return DOMAIN_SOURCED_PACKETS;
}
};
using PacketType = PacketTypeEnum::Value;

View file

@ -58,6 +58,7 @@ void RenderEventHandler::onInitalize() {
return;
}
_canvas.setThreadContext();
if (!_canvas.makeCurrent()) {
qFatal("Unable to make QML rendering context current on render thread");
}

View file

@ -77,7 +77,7 @@ void MeshPartPayload::removeMaterial(graphics::MaterialPointer material) {
_drawMaterials.remove(material);
}
void MeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tagBits) {
void MeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled) {
ItemKey::Builder builder;
builder.withTypeShape();
@ -91,6 +91,10 @@ void MeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tagBits)
builder.withLayered();
}
if (isGroupCulled) {
builder.withSubMetaCulled();
}
if (_drawMaterials.top().material) {
auto matKey = _drawMaterials.top().material->getKey();
if (matKey.isTranslucent()) {
@ -286,7 +290,7 @@ void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& render
_worldBound.transform(boundTransform);
}
void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tagBits) {
void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled) {
ItemKey::Builder builder;
builder.withTypeShape();
@ -300,6 +304,10 @@ void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, uint8_t tag
builder.withLayered();
}
if (isGroupCulled) {
builder.withSubMetaCulled();
}
if (_isBlendShaped || _isSkinned) {
builder.withDeformed();
}

View file

@ -32,7 +32,7 @@ public:
typedef render::Payload<MeshPartPayload> Payload;
typedef Payload::DataPointer Pointer;
virtual void updateKey(bool isVisible, bool isLayered, uint8_t tagBits);
virtual void updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled = false);
virtual void updateMeshPart(const std::shared_ptr<const graphics::Mesh>& drawMesh, int partIndex);
@ -98,7 +98,7 @@ public:
using TransformType = glm::mat4;
#endif
void updateKey(bool isVisible, bool isLayered, uint8_t tagBits) override;
void updateKey(bool isVisible, bool isLayered, uint8_t tagBits, bool isGroupCulled = false) override;
void updateClusterBuffer(const std::vector<TransformType>& clusterTransforms);
void updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform);

View file

@ -271,6 +271,7 @@ void Model::updateRenderItems() {
uint8_t viewTagBits = self->getViewTagBits();
bool isLayeredInFront = self->isLayeredInFront();
bool isLayeredInHUD = self->isLayeredInHUD();
bool isGroupCulled = self->isGroupCulled();
render::Transaction transaction;
for (int i = 0; i < (int) self->_modelMeshRenderItemIDs.size(); i++) {
@ -284,7 +285,7 @@ void Model::updateRenderItems() {
transaction.updateItem<ModelMeshPartPayload>(itemID, [modelTransform, clusterTransforms,
invalidatePayloadShapeKey, isWireframe, isVisible,
viewTagBits, isLayeredInFront,
isLayeredInHUD](ModelMeshPartPayload& data) {
isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) {
data.updateClusterBuffer(clusterTransforms);
Transform renderTransform = modelTransform;
@ -300,7 +301,7 @@ void Model::updateRenderItems() {
}
data.updateTransformForSkinnedMesh(renderTransform, modelTransform);
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits);
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled);
data.setLayer(isLayeredInFront, isLayeredInHUD);
data.setShapeKey(invalidatePayloadShapeKey, isWireframe);
});
@ -684,10 +685,11 @@ void Model::calculateTriangleSets() {
}
}
void Model::setVisibleInScene(bool isVisible, const render::ScenePointer& scene, uint8_t viewTagBits) {
if (_isVisible != isVisible || _viewTagBits != viewTagBits) {
void Model::setVisibleInScene(bool isVisible, const render::ScenePointer& scene, uint8_t viewTagBits, bool isGroupCulled) {
if (_isVisible != isVisible || _viewTagBits != viewTagBits || _isGroupCulled != isGroupCulled) {
_isVisible = isVisible;
_viewTagBits = viewTagBits;
_isGroupCulled = isGroupCulled;
bool isLayeredInFront = _isLayeredInFront;
bool isLayeredInHUD = _isLayeredInHUD;
@ -695,14 +697,14 @@ void Model::setVisibleInScene(bool isVisible, const render::ScenePointer& scene,
render::Transaction transaction;
foreach (auto item, _modelMeshRenderItemsMap.keys()) {
transaction.updateItem<ModelMeshPartPayload>(item, [isVisible, viewTagBits, isLayeredInFront,
isLayeredInHUD](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits);
isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled);
});
}
foreach(auto item, _collisionRenderItemsMap.keys()) {
transaction.updateItem<ModelMeshPartPayload>(item, [isVisible, viewTagBits, isLayeredInFront,
isLayeredInHUD](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits);
isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled);
});
}
scene->enqueueTransaction(transaction);
@ -717,19 +719,20 @@ void Model::setLayeredInFront(bool isLayeredInFront, const render::ScenePointer&
bool isVisible = _isVisible;
uint8_t viewTagBits = _viewTagBits;
bool isLayeredInHUD = _isLayeredInHUD;
bool isGroupCulled = _isGroupCulled;
render::Transaction transaction;
foreach(auto item, _modelMeshRenderItemsMap.keys()) {
transaction.updateItem<ModelMeshPartPayload>(item, [isVisible, viewTagBits, isLayeredInFront,
isLayeredInHUD](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits);
isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled);
data.setLayer(isLayeredInFront, isLayeredInHUD);
});
}
foreach(auto item, _collisionRenderItemsMap.keys()) {
transaction.updateItem<ModelMeshPartPayload>(item, [isVisible, viewTagBits, isLayeredInFront,
isLayeredInHUD](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits);
isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled);
data.setLayer(isLayeredInFront, isLayeredInHUD);
});
}
@ -744,19 +747,20 @@ void Model::setLayeredInHUD(bool isLayeredInHUD, const render::ScenePointer& sce
bool isVisible = _isVisible;
uint8_t viewTagBits = _viewTagBits;
bool isLayeredInFront = _isLayeredInFront;
bool isGroupCulled = _isGroupCulled;
render::Transaction transaction;
foreach(auto item, _modelMeshRenderItemsMap.keys()) {
transaction.updateItem<ModelMeshPartPayload>(item, [isVisible, viewTagBits, isLayeredInFront,
isLayeredInHUD](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits);
isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled);
data.setLayer(isLayeredInFront, isLayeredInHUD);
});
}
foreach(auto item, _collisionRenderItemsMap.keys()) {
transaction.updateItem<ModelMeshPartPayload>(item, [isVisible, viewTagBits, isLayeredInFront,
isLayeredInHUD](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits);
isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) {
data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, viewTagBits, isGroupCulled);
data.setLayer(isLayeredInFront, isLayeredInHUD);
});
}

View file

@ -86,7 +86,7 @@ public:
const QUrl& getURL() const { return _url; }
// new Scene/Engine rendering support
void setVisibleInScene(bool isVisible, const render::ScenePointer& scene, uint8_t viewTagBits);
void setVisibleInScene(bool isVisible, const render::ScenePointer& scene, uint8_t viewTagBits, bool isGroupCulled);
void setLayeredInFront(bool isLayeredInFront, const render::ScenePointer& scene);
void setLayeredInHUD(bool isLayeredInHUD, const render::ScenePointer& scene);
bool needsFixupInScene() const;
@ -109,6 +109,8 @@ public:
bool isLayeredInFront() const { return _isLayeredInFront; }
bool isLayeredInHUD() const { return _isLayeredInHUD; }
bool isGroupCulled() const { return _isGroupCulled; }
virtual void updateRenderItems();
void setRenderItemsNeedUpdate();
bool getRenderItemsNeedUpdate() { return _renderItemsNeedUpdate; }
@ -466,6 +468,8 @@ protected:
bool _isLayeredInFront { false };
bool _isLayeredInHUD { false };
bool _isGroupCulled{ false };
bool shouldInvalidatePayloadShapeKey(int meshIndex);
private:

View file

@ -211,13 +211,14 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
outItems.clear();
outItems.reserve(inSelection.numItems());
const auto filter = inputs.get1();
if (!filter.selectsNothing()) {
// Now get the bound, and
const auto srcFilter = inputs.get1();
if (!srcFilter.selectsNothing()) {
auto filter = render::ItemFilter::Builder(srcFilter).withoutSubMetaCulled().build();
// Now get the bound, and
// filter individually against the _filter
// visibility cull if partially selected ( octree cell contianing it was partial)
// distance cull if was a subcell item ( octree cell is way bigger than the item bound itself, so now need to test per item)
if (_skipCulling) {
// inside & fit items: filter only, culling is disabled
{
@ -227,6 +228,9 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
if (filter.test(item.getKey())) {
ItemBound itemBound(id, item.getBound());
outItems.emplace_back(itemBound);
if (item.getKey().isMetaCullGroup()) {
item.fetchMetaSubItemBounds(outItems, (*scene));
}
}
}
}
@ -239,6 +243,9 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
if (filter.test(item.getKey())) {
ItemBound itemBound(id, item.getBound());
outItems.emplace_back(itemBound);
if (item.getKey().isMetaCullGroup()) {
item.fetchMetaSubItemBounds(outItems, (*scene));
}
}
}
}
@ -251,6 +258,9 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
if (filter.test(item.getKey())) {
ItemBound itemBound(id, item.getBound());
outItems.emplace_back(itemBound);
if (item.getKey().isMetaCullGroup()) {
item.fetchMetaSubItemBounds(outItems, (*scene));
}
}
}
}
@ -263,6 +273,9 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
if (filter.test(item.getKey())) {
ItemBound itemBound(id, item.getBound());
outItems.emplace_back(itemBound);
if (item.getKey().isMetaCullGroup()) {
item.fetchMetaSubItemBounds(outItems, (*scene));
}
}
}
}
@ -277,6 +290,9 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
if (filter.test(item.getKey())) {
ItemBound itemBound(id, item.getBound());
outItems.emplace_back(itemBound);
if (item.getKey().isMetaCullGroup()) {
item.fetchMetaSubItemBounds(outItems, (*scene));
}
}
}
}
@ -290,6 +306,9 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
ItemBound itemBound(id, item.getBound());
if (test.solidAngleTest(itemBound.bound)) {
outItems.emplace_back(itemBound);
if (item.getKey().isMetaCullGroup()) {
item.fetchMetaSubItemBounds(outItems, (*scene));
}
}
}
}
@ -304,6 +323,9 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
ItemBound itemBound(id, item.getBound());
if (test.frustumTest(itemBound.bound)) {
outItems.emplace_back(itemBound);
if (item.getKey().isMetaCullGroup()) {
item.fetchMetaSubItemBounds(outItems, (*scene));
}
}
}
}
@ -319,6 +341,9 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
if (test.frustumTest(itemBound.bound)) {
if (test.solidAngleTest(itemBound.bound)) {
outItems.emplace_back(itemBound);
if (item.getKey().isMetaCullGroup()) {
item.fetchMetaSubItemBounds(outItems, (*scene));
}
}
}
}

View file

@ -113,6 +113,21 @@ const ShapeKey Item::getShapeKey() const {
return shapeKey;
}
uint32_t Item::fetchMetaSubItemBounds(ItemBounds& subItemBounds, Scene& scene) const {
ItemIDs subItems;
auto numSubs = fetchMetaSubItems(subItems);
for (auto id : subItems) {
auto& item = scene.getItem(id);
if (item.exist()) {
subItemBounds.emplace_back(id, item.getBound());
} else {
numSubs--;
}
}
return numSubs;
}
namespace render {
template <> const ItemKey payloadGetKey(const PayloadProxyInterface::Pointer& payload) {
if (!payload) {

View file

@ -78,6 +78,8 @@ public:
INVISIBLE, // Visible or not in the scene?
SHADOW_CASTER, // Item cast shadows
LAYERED, // Item belongs to one of the layers different from the default layer
META_CULL_GROUP, // As a meta item, the culling of my sub items is based solely on my bounding box and my visibility in the view
SUB_META_CULLED, // As a sub item of a meta render item set as cull group, need to be set to my culling to the meta render it
FIRST_TAG_BIT, // 8 Tags available to organize the items and filter them against
LAST_TAG_BIT = FIRST_TAG_BIT + NUM_TAGS,
@ -122,6 +124,8 @@ public:
Builder& withInvisible() { _flags.set(INVISIBLE); return (*this); }
Builder& withShadowCaster() { _flags.set(SHADOW_CASTER); return (*this); }
Builder& withLayered() { _flags.set(LAYERED); return (*this); }
Builder& withMetaCullGroup() { _flags.set(META_CULL_GROUP); return (*this); }
Builder& withSubMetaCulled() { _flags.set(SUB_META_CULLED); return (*this); }
Builder& withTag(Tag tag) { _flags.set(FIRST_TAG_BIT + tag); return (*this); }
// Set ALL the tags in one call using the Tag bits
@ -159,6 +163,12 @@ public:
bool isLayered() const { return _flags[LAYERED]; }
bool isSpatial() const { return !isLayered(); }
bool isMetaCullGroup() const { return _flags[META_CULL_GROUP]; }
void setMetaCullGroup(bool cullGroup) { (cullGroup ? _flags.set(META_CULL_GROUP) : _flags.reset(META_CULL_GROUP)); }
bool isSubMetaCulled() const { return _flags[SUB_META_CULLED]; }
void setSubMetaCulled(bool metaCulled) { (metaCulled ? _flags.set(SUB_META_CULLED) : _flags.reset(SUB_META_CULLED)); }
bool isTag(Tag tag) const { return _flags[FIRST_TAG_BIT + tag]; }
uint8_t getTagBits() const { return ((_flags.to_ulong() & KEY_TAG_BITS_MASK) >> FIRST_TAG_BIT); }
@ -193,6 +203,7 @@ public:
ItemKey::Flags _mask{ 0 };
public:
Builder() {}
Builder(const ItemFilter& srcFilter) : _value(srcFilter._value), _mask(srcFilter._mask) {}
ItemFilter build() const { return ItemFilter(_value, _mask); }
@ -221,6 +232,12 @@ public:
Builder& withoutLayered() { _value.reset(ItemKey::LAYERED); _mask.set(ItemKey::LAYERED); return (*this); }
Builder& withLayered() { _value.set(ItemKey::LAYERED); _mask.set(ItemKey::LAYERED); return (*this); }
Builder& withoutMetaCullGroup() { _value.reset(ItemKey::META_CULL_GROUP); _mask.set(ItemKey::META_CULL_GROUP); return (*this); }
Builder& withMetaCullGroup() { _value.set(ItemKey::META_CULL_GROUP); _mask.set(ItemKey::META_CULL_GROUP); return (*this); }
Builder& withoutSubMetaCulled() { _value.reset(ItemKey::SUB_META_CULLED); _mask.set(ItemKey::SUB_META_CULLED); return (*this); }
Builder& withSubMetaCulled() { _value.set(ItemKey::SUB_META_CULLED); _mask.set(ItemKey::SUB_META_CULLED); return (*this); }
Builder& withoutTag(ItemKey::Tag tagIndex) { _value.reset(ItemKey::FIRST_TAG_BIT + tagIndex); _mask.set(ItemKey::FIRST_TAG_BIT + tagIndex); return (*this); }
Builder& withTag(ItemKey::Tag tagIndex) { _value.set(ItemKey::FIRST_TAG_BIT + tagIndex); _mask.set(ItemKey::FIRST_TAG_BIT + tagIndex); return (*this); }
// Set ALL the tags in one call using the Tag bits and the Tag bits touched
@ -420,6 +437,7 @@ public:
// Meta Type Interface
uint32_t fetchMetaSubItems(ItemIDs& subItems) const { return _payload->fetchMetaSubItems(subItems); }
uint32_t fetchMetaSubItemBounds(ItemBounds& subItemBounds, Scene& scene) const;
// Access the status
const StatusPointer& getStatus() const { return _payload->getStatus(); }

View file

@ -30,7 +30,7 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin
const auto culledSpatialSelection = task.addJob<CullSpatialSelection>("CullSceneSelection", cullInputs, cullFunctor, RenderDetails::ITEM);
// Overlays are not culled
const ItemFilter overlayfilter = ItemFilter::Builder().withVisible().withTagBits(tagBits, tagMask);
const ItemFilter overlayfilter = ItemFilter::Builder().withVisible().withoutSubMetaCulled().withTagBits(tagBits, tagMask);
const auto nonspatialFilter = render::Varying(overlayfilter);
const auto nonspatialSelection = task.addJob<FetchNonspatialItems>("FetchOverlaySelection", nonspatialFilter);

View file

@ -207,6 +207,10 @@ public:
void dump(const QString& prefix = "") const;
virtual void locationChanged(bool tellPhysics = true); // called when a this object's location has changed
virtual void dimensionsChanged() { _queryAACubeSet = false; } // called when a this object's dimensions have changed
virtual void parentDeleted() { } // called on children of a deleted parent
protected:
const NestableType _nestableType; // EntityItem or an AvatarData
QUuid _id;
@ -218,10 +222,6 @@ protected:
mutable ReadWriteLockable _childrenLock;
mutable QHash<QUuid, SpatiallyNestableWeakPointer> _children;
virtual void locationChanged(bool tellPhysics = true); // called when a this object's location has changed
virtual void dimensionsChanged() { _queryAACubeSet = false; } // called when a this object's dimensions have changed
virtual void parentDeleted() { } // called on children of a deleted parent
// _queryAACube is used to decide where something lives in the octree
mutable AACube _queryAACube;
mutable bool _queryAACubeSet { false };

View file

@ -485,7 +485,7 @@ bool OpenVrDisplayPlugin::internalActivate() {
if (_threadedSubmit) {
_submitThread = std::make_shared<OpenVrSubmitThread>(*this);
if (!_submitCanvas) {
withMainThreadContext([&] {
withOtherThreadContext([&] {
_submitCanvas = std::make_shared<gl::OffscreenContext>();
_submitCanvas->create();
_submitCanvas->doneCurrent();

View file

@ -834,10 +834,11 @@ function loaded() {
// HTML workaround since image is not yet a separate entity type
var IMAGE_MODEL_NAME = 'default-image-model.fbx';
if (properties.modelURL) {
var urlParts = properties.modelURL.split('/')
if (properties.type === "Model") {
var urlParts = properties.modelURL.split('/');
var propsFilename = urlParts[urlParts.length - 1];
if (properties.type === "Model" && propsFilename === IMAGE_MODEL_NAME) {
if (propsFilename === IMAGE_MODEL_NAME) {
properties.type = "Image";
}
}

View file

@ -47,7 +47,7 @@ function calcSpawnInfo(hand, landscape) {
var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position;
var headRot = (HMD.active && Camera.mode === "first person") ? HMD.orientation : Camera.orientation;
var forward = Quat.getForward(headRot);
var forward = Quat.getForward(Quat.cancelOutRollAndPitch(headRot));
var FORWARD_OFFSET = 0.5 * MyAvatar.sensorToWorldScale;
finalPosition = Vec3.sum(headPos, Vec3.multiply(FORWARD_OFFSET, forward));
var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, forward, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.UNIT_Y));
@ -269,8 +269,9 @@ WebTablet.prototype.setLandscape = function(newLandscapeValue) {
}
this.landscape = newLandscapeValue;
var cameraOrientation = Quat.cancelOutRollAndPitch(Camera.orientation);
Overlays.editOverlay(this.tabletEntityID,
{ rotation: Quat.multiply(Camera.orientation, this.landscape ? ROT_LANDSCAPE : ROT_Y_180) });
{ rotation: Quat.multiply(cameraOrientation, this.landscape ? ROT_LANDSCAPE : ROT_Y_180) });
var tabletWidth = getTabletWidthFromSettings() * MyAvatar.sensorToWorldScale;
var tabletScaleFactor = tabletWidth / TABLET_NATURAL_DIMENSIONS.x;
@ -278,7 +279,7 @@ WebTablet.prototype.setLandscape = function(newLandscapeValue) {
var screenWidth = 0.82 * tabletWidth;
var screenHeight = 0.81 * tabletHeight;
Overlays.editOverlay(this.webOverlayID, {
rotation: Quat.multiply(Camera.orientation, ROT_LANDSCAPE_WINDOW),
rotation: Quat.multiply(cameraOrientation, ROT_LANDSCAPE_WINDOW),
dimensions: {x: this.landscape ? screenHeight : screenWidth, y: this.landscape ? screenWidth : screenHeight, z: 0.1}
});
};

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@
""
],
"devDependencies": {
"electron-packager": "^6.0.2",
"electron-packager": "^11.0.0",
"electron-prebuilt": "0.37.5"
},
"repository": {
@ -27,7 +27,7 @@
"cheerio": "^0.19.0",
"extend": "^3.0.0",
"fs-extra": "^0.26.4",
"node-notifier": "^4.4.0",
"node-notifier": "^5.2.1",
"os-homedir": "^1.0.1",
"request": "^2.67.0",
"request-progress": "1.0.2",