3
0
Fork 0
mirror of https://github.com/lubosz/overte.git synced 2025-04-15 09:49:57 +02:00

Merge branch 'master' of https://github.com/highfidelity/hifi into discovery

This commit is contained in:
howard-stearns 2016-08-11 15:40:26 -07:00
commit e6746acf4c
179 changed files with 6158 additions and 3579 deletions
.eslintrc.js
assignment-client/src/audio
cmake/macros
domain-server
interface
libraries

View file

@ -72,6 +72,6 @@ module.exports = {
"spaced-comment": ["error", "always", {
"line": { "markers": ["/"] }
}],
"space-before-function-paren": ["error", {"anonymous": "always", "named": "never"}]
"space-before-function-paren": ["error", {"anonymous": "ignore", "named": "never"}]
}
};

View file

@ -100,6 +100,63 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f;
const int IEEE754_MANT_BITS = 23;
const int IEEE754_EXPN_BIAS = 127;
//
// for x > 0.0f, returns log2(x)
// for x <= 0.0f, returns large negative value
//
// abs |error| < 8e-3, smooth (exact for x=2^N) for NPOLY=3
// abs |error| < 2e-4, smooth (exact for x=2^N) for NPOLY=5
// rel |error| < 0.4 from precision loss very close to 1.0f
//
static inline float fastlog2(float x) {
union { float f; int32_t i; } mant, bits = { x };
// split into mantissa and exponent
mant.i = (bits.i & ((1 << IEEE754_MANT_BITS) - 1)) | (IEEE754_EXPN_BIAS << IEEE754_MANT_BITS);
int32_t expn = (bits.i >> IEEE754_MANT_BITS) - IEEE754_EXPN_BIAS;
mant.f -= 1.0f;
// polynomial for log2(1+x) over x=[0,1]
//x = (-0.346555386f * mant.f + 1.346555386f) * mant.f;
x = (((-0.0821307180f * mant.f + 0.321188984f) * mant.f - 0.677784014f) * mant.f + 1.43872575f) * mant.f;
return x + expn;
}
//
// for -126 <= x < 128, returns exp2(x)
//
// rel |error| < 3e-3, smooth (exact for x=N) for NPOLY=3
// rel |error| < 9e-6, smooth (exact for x=N) for NPOLY=5
//
static inline float fastexp2(float x) {
union { float f; int32_t i; } xi;
// bias such that x > 0
x += IEEE754_EXPN_BIAS;
//x = MAX(x, 1.0f);
//x = MIN(x, 254.9999f);
// split into integer and fraction
xi.i = (int32_t)x;
x -= xi.i;
// construct exp2(xi) as a float
xi.i <<= IEEE754_MANT_BITS;
// polynomial for exp2(x) over x=[0,1]
//x = (0.339766028f * x + 0.660233972f) * x + 1.0f;
x = (((0.0135557472f * x + 0.0520323690f) * x + 0.241379763f) * x + 0.693032121f) * x + 1.0f;
return x * xi.f;
}
float AudioMixer::gainForSource(const PositionalAudioStream& streamToAdd,
const AvatarAudioStream& listeningNodeStream, const glm::vec3& relativePosition, bool isEcho) {
float gain = 1.0f;
@ -148,7 +205,7 @@ float AudioMixer::gainForSource(const PositionalAudioStream& streamToAdd,
g = (g > 1.0f) ? 1.0f : g;
// calculate the distance coefficient using the distance to this node
float distanceCoefficient = exp2f(log2f(g) * log2f(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE));
float distanceCoefficient = fastexp2(fastlog2(g) * fastlog2(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE));
// multiply the current attenuation coefficient by the distance coefficient
gain *= distanceCoefficient;

View file

@ -113,7 +113,7 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) {
avatarAudioStream->setupCodec(_codec, _selectedCodecName, AudioConstants::MONO);
qDebug() << "creating new AvatarAudioStream... codec:" << _selectedCodecName;
connect(avatarAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioMixerClientData::sendSelectAudioFormat);
connect(avatarAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioMixerClientData::handleMismatchAudioFormat);
auto emplaced = _audioStreams.emplace(
QUuid(),
@ -345,6 +345,11 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() {
return result;
}
void AudioMixerClientData::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) {
qDebug() << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec;
sendSelectAudioFormat(node, currentCodec);
}
void AudioMixerClientData::sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName) {
auto replyPacket = NLPacket::create(PacketType::SelectedAudioFormat);
replyPacket->writeString(selectedCodecName);

View file

@ -84,6 +84,7 @@ signals:
void injectorStreamFinished(const QUuid& streamIdentifier);
public slots:
void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec);
void sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName);
private:

View file

@ -23,13 +23,13 @@ function(AUTOSCRIBE_SHADER SHADER_FILE)
#Extract the unique include shader paths
set(INCLUDES ${HIFI_LIBRARIES_SHADER_INCLUDE_FILES})
#message(Hifi for includes ${INCLUDES})
foreach(EXTRA_SHADER_INCLUDE ${INCLUDES})
#message(${TARGET_NAME} Hifi for includes ${INCLUDES})
foreach(EXTRA_SHADER_INCLUDE ${INCLUDES})
list(APPEND SHADER_INCLUDES_PATHS ${EXTRA_SHADER_INCLUDE})
endforeach()
list(REMOVE_DUPLICATES SHADER_INCLUDES_PATHS)
#message(ready for includes ${SHADER_INCLUDES_PATHS})
#message(ready for includes ${SHADER_INCLUDES_PATHS})
# make the scribe include arguments
set(SCRIBE_INCLUDES)
@ -77,6 +77,7 @@ endfunction()
macro(AUTOSCRIBE_SHADER_LIB)
set(HIFI_LIBRARIES_SHADER_INCLUDE_FILES "")
file(RELATIVE_PATH RELATIVE_LIBRARY_DIR_PATH ${CMAKE_CURRENT_SOURCE_DIR} "${HIFI_LIBRARY_DIR}")
foreach(HIFI_LIBRARY ${ARGN})
#if (NOT TARGET ${HIFI_LIBRARY})
@ -86,7 +87,7 @@ macro(AUTOSCRIBE_SHADER_LIB)
#file(GLOB_RECURSE HIFI_LIBRARIES_SHADER_INCLUDE_FILES ${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src/*.slh)
list(APPEND HIFI_LIBRARIES_SHADER_INCLUDE_FILES ${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src)
endforeach()
#message(${HIFI_LIBRARIES_SHADER_INCLUDE_FILES})
#message("${TARGET_NAME} ${HIFI_LIBRARIES_SHADER_INCLUDE_FILES}")
file(GLOB_RECURSE SHADER_INCLUDE_FILES src/*.slh)
file(GLOB_RECURSE SHADER_SOURCE_FILES src/*.slv src/*.slf src/*.slg)
@ -95,13 +96,14 @@ macro(AUTOSCRIBE_SHADER_LIB)
set(SHADERS_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders/${TARGET_NAME}")
file(MAKE_DIRECTORY ${SHADERS_DIR})
#message(${SHADER_INCLUDE_FILES})
#message("${TARGET_NAME} ${SHADER_INCLUDE_FILES}")
set(AUTOSCRIBE_SHADER_SRC "")
foreach(SHADER_FILE ${SHADER_SOURCE_FILES})
AUTOSCRIBE_SHADER(${SHADER_FILE} ${SHADER_INCLUDE_FILES})
file(TO_CMAKE_PATH "${AUTOSCRIBE_SHADER_RETURN}" AUTOSCRIBE_GENERATED_FILE)
list(APPEND AUTOSCRIBE_SHADER_SRC ${AUTOSCRIBE_GENERATED_FILE})
endforeach()
#message(${AUTOSCRIBE_SHADER_SRC})
#message(${TARGET_NAME} ${AUTOSCRIBE_SHADER_SRC})
if (WIN32)
source_group("Shaders" FILES ${SHADER_INCLUDE_FILES})
@ -116,4 +118,4 @@ macro(AUTOSCRIBE_SHADER_LIB)
# Link library shaders, if they exist
include_directories("${SHADERS_DIR}")
endmacro()
endmacro()

View file

@ -54,8 +54,9 @@ macro(SETUP_HIFI_LIBRARY)
target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE})
endforeach()
# Don't make scribed shaders cumulative
# Don't make scribed shaders or QT resource files cumulative
set(AUTOSCRIBE_SHADER_LIB_SRC "")
set(QT_RESOURCES_FILE "")
target_glm()

View file

@ -1,5 +1,5 @@
{
"version": 1.5,
"version": 1.7,
"settings": [
{
"name": "metaverse",
@ -384,18 +384,18 @@
"name": "standard_permissions",
"type": "table",
"label": "Domain-Wide User Permissions",
"help": "Indicate which users or groups can have which <a data-toggle='tooltip' data-html=true title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>domain-wide permissions</a>.",
"help": "Indicate which types of users can have which <a data-toggle='tooltip' data-html=true title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>domain-wide permissions</a>.",
"caption": "Standard Permissions",
"can_add_new_rows": false,
"groups": [
{
"label": "User / Group",
"label": "Type of User",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>?</a>",
"span": 6
"span": 7
}
],
@ -409,7 +409,7 @@
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": true
"default": false
},
{
"name": "id_can_adjust_locks",
@ -445,6 +445,13 @@
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_kick",
"label": "Kick Users",
"type": "checkbox",
"editable": true,
"default": false
}
],
@ -452,33 +459,62 @@
"non-deletable-row-values": ["localhost", "anonymous", "logged-in"]
},
{
"name": "permissions",
"name": "group_permissions",
"type": "table",
"caption": "Permissions for Specific Users",
"can_add_new_rows": true,
"caption": "Permissions for Users in Groups",
"categorize_by_key": "permissions_id",
"can_add_new_categories": true,
"can_add_new_rows": false,
"new_category_placeholder": "Add Group",
"new_category_message": "Save and reload to see ranks",
"groups": [
{
"label": "User / Group",
"label": "Rank",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>?</a>",
"span": 6
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users in specific groups can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users in specific groups can change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether users in specific groups can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users in specific groups can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users in specific groups can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether user in specific groups can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Permissions granted to a specific user will be a union of the permissions granted to the groups they are in, as well as permissions from the previous section. Group permissions are only granted if the user doesn&rsquo;t have their own row in the per-account section, below.</p>'>?</a>",
"span": 7
}
],
"columns": [
{
"name": "permissions_id",
"label": ""
"label": "Group Name",
"readonly": true,
"hidden": true
},
{
"name": "rank_id",
"label": "Rank ID",
"readonly": true,
"hidden": true
},
{
"name": "rank_order",
"label": "Rank Order",
"readonly": true,
"hidden": true
},
{
"name": "rank_name",
"label": "",
"readonly": true
},
{
"name": "group_id",
"label": "Group ID",
"readonly": true,
"hidden": true
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": true
"default": false
},
{
"name": "id_can_adjust_locks",
@ -514,6 +550,257 @@
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_kick",
"label": "Kick Users",
"type": "checkbox",
"editable": true,
"default": false
}
]
},
{
"name": "group_forbiddens",
"type": "table",
"caption": "Permissions Denied to Users in Groups",
"categorize_by_key": "permissions_id",
"can_add_new_categories": true,
"can_add_new_rows": false,
"new_category_placeholder": "Add Blacklist Group",
"new_category_message": "Save and reload to see ranks",
"groups": [
{
"label": "Rank",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users in specific groups can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users in specific groups can change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether users in specific groups can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users in specific groups can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users in specific groups can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether user in specific groups can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Permissions granted to a specific user will be a union of the permissions granted to the groups they are in. Group permissions are only granted if the user doesn&rsquo;t have their own row in the per-account section, below.</p>'>?</a>",
"span": 7
}
],
"columns": [
{
"name": "permissions_id",
"label": "Group Name",
"hidden": true
},
{
"name": "rank_id",
"label": "Rank ID",
"hidden": true
},
{
"name": "rank_order",
"label": "Rank Order",
"hidden": true
},
{
"name": "rank_name",
"label": "",
"readonly": true
},
{
"name": "group_id",
"label": "Group ID",
"readonly": true,
"hidden": true
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_kick",
"label": "Kick Users",
"type": "checkbox",
"editable": true,
"default": false
}
]
},
{
"name": "ip_permissions",
"type": "table",
"caption": "Permissions for Users from IP Addresses",
"can_add_new_rows": true,
"groups": [
{
"label": "IP Address",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide IP Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users from specific IPs can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users from specific IPs can change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether users from specific IPs can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users from specific IPs can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users from specific IPs can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether users from specific IPs can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific IP will supersede any parameter-level permissions that might otherwise apply to that user (from groups or standard permissions above). IP address permissions are overriden if the user has their own row in the users section.</p>'>?</a>",
"span": 7
}
],
"columns": [
{
"name": "permissions_id",
"label": ""
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_kick",
"label": "Kick Users",
"type": "checkbox",
"editable": true,
"default": false
}
]
},
{
"name": "permissions",
"type": "table",
"caption": "Permissions for Specific Users",
"can_add_new_rows": true,
"groups": [
{
"label": "User",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level or group permissions that might otherwise apply to that user.</p>'>?</a>",
"span": 7
}
],
"columns": [
{
"name": "permissions_id",
"label": ""
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_kick",
"label": "Kick Users",
"type": "checkbox",
"editable": true,
"default": false
}
]
}

View file

@ -3,6 +3,10 @@ body {
padding-bottom: 30px;
}
[hidden] {
display: none !important;
}
.table-lead .lead-line {
background-color: black;
}
@ -20,7 +24,9 @@ body {
top: 40px;
}
.table .value-row td, .table .inputs td {
.table .value-row td,
.table .value-category td,
.table .inputs td {
vertical-align: middle;
}
@ -31,6 +37,31 @@ body {
margin-right: auto;
}
.value-category:not(.inputs) {
font-weight: bold;
background: #f5f5f5;
}
.table .value-category [message]::after {
content: attr(message);
font-style: italic;
font-weight: normal;
}
.table .value-row.contracted,
.table .inputs.contracted {
display: none;
}
.toggle-category {
cursor: pointer;
}
.toggle-category-icon {
padding: 4px;
margin-right: 8px;
}
.glyphicon-remove {
font-size: 24px;
}
@ -44,15 +75,6 @@ span.port {
color: #666666;
}
.locked {
color: #428bca;
}
.locked-table {
cursor: not-allowed;
background-color: #eee;
}
.advanced-setting {
display: none;
}
@ -133,7 +155,7 @@ table .headers + .headers td {
color: #222;
}
table[name="security.standard_permissions"] .headers td + td, table[name="security.permissions"] .headers td + td {
#security table .headers td + td {
text-align: center;
}

View file

@ -25,7 +25,7 @@
<ul class="nav nav-pills nav-stacked">
</ul>
<button id="advanced-toggle-button" hidden=true class="btn btn-info advanced-toggle">Show advanced</button>
<button id="advanced-toggle-button" class="btn btn-info advanced-toggle">Show advanced</button>
<button class="btn btn-success save-button">Save and restart</button>
</div>
</div>
@ -57,15 +57,13 @@
<div class="panel-body">
<% _.each(split_settings[0], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, false,
(_.has(locked, group.name) && _.has(locked[group.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,
(_.has(locked, group.name) && _.has(locked[group.name], setting.name))) %>
<%= getFormGroup(keypath, setting, values, true) %>
<% }); %>
<% }%>
</div>

View file

@ -5,10 +5,20 @@ var Settings = {
TRIGGER_CHANGE_CLASS: 'trigger-change',
DATA_ROW_CLASS: 'value-row',
DATA_COL_CLASS: 'value-col',
DATA_CATEGORY_CLASS: 'value-category',
ADD_ROW_BUTTON_CLASS: 'add-row',
ADD_ROW_SPAN_CLASSES: 'glyphicon glyphicon-plus add-row',
DEL_ROW_BUTTON_CLASS: 'del-row',
DEL_ROW_SPAN_CLASSES: 'glyphicon glyphicon-remove del-row',
ADD_CATEGORY_BUTTON_CLASS: 'add-category',
ADD_CATEGORY_SPAN_CLASSES: 'glyphicon glyphicon-plus add-category',
TOGGLE_CATEGORY_COLUMN_CLASS: 'toggle-category',
TOGGLE_CATEGORY_SPAN_CLASS: 'toggle-category-icon',
TOGGLE_CATEGORY_SPAN_CLASSES: 'glyphicon toggle-category-icon',
TOGGLE_CATEGORY_EXPANDED_CLASS: 'glyphicon-triangle-bottom',
TOGGLE_CATEGORY_CONTRACTED_CLASS: 'glyphicon-triangle-right',
DEL_CATEGORY_BUTTON_CLASS: 'del-category',
DEL_CATEGORY_SPAN_CLASSES: 'glyphicon glyphicon-remove del-category',
MOVE_UP_BUTTON_CLASS: 'move-up',
MOVE_UP_SPAN_CLASSES: 'glyphicon glyphicon-chevron-up move-up',
MOVE_DOWN_BUTTON_CLASS: 'move-down',
@ -31,11 +41,11 @@ var Settings = {
};
var viewHelpers = {
getFormGroup: function(keypath, setting, values, isAdvanced, isLocked) {
getFormGroup: function(keypath, setting, values, isAdvanced) {
form_group = "<div class='form-group " + (isAdvanced ? Settings.ADVANCED_CLASS : "") + "' data-keypath='" + keypath + "'>";
setting_value = _(values).valueForKeyPath(keypath);
if (typeof setting_value == 'undefined' || setting_value === null) {
if (_.isUndefined(setting_value) || _.isNull(setting_value)) {
if (_.has(setting, 'default')) {
setting_value = setting.default;
} else {
@ -44,16 +54,13 @@ var viewHelpers = {
}
label_class = 'control-label';
if (isLocked) {
label_class += ' locked';
}
function common_attrs(extra_classes) {
extra_classes = (typeof extra_classes !== 'undefined' ? extra_classes : "");
extra_classes = (!_.isUndefined(extra_classes) ? extra_classes : "");
return " class='" + (setting.type !== 'checkbox' ? 'form-control' : '')
+ " " + Settings.TRIGGER_CHANGE_CLASS + " " + extra_classes + "' data-short-name='"
+ setting.name + "' name='" + keypath + "' "
+ "id='" + (typeof setting.html_id !== 'undefined' ? setting.html_id : keypath) + "'";
+ "id='" + (!_.isUndefined(setting.html_id) ? setting.html_id : keypath) + "'";
}
if (setting.type === 'checkbox') {
@ -61,9 +68,8 @@ var viewHelpers = {
form_group += "<label class='" + label_class + "'>" + setting.label + "</label>"
}
form_group += "<div class='toggle-checkbox-container" + (isLocked ? " disabled" : "") + "'>"
form_group += "<input type='checkbox'" + common_attrs('toggle-checkbox') + (setting_value ? "checked" : "")
form_group += (isLocked ? " disabled" : "") + "/>"
form_group += "<div class='toggle-checkbox-container'>"
form_group += "<input type='checkbox'" + common_attrs('toggle-checkbox') + (setting_value ? "checked" : "") + "/>"
if (setting.help) {
form_group += "<span class='help-block checkbox-help'>" + setting.help + "</span>";
@ -78,7 +84,7 @@ var viewHelpers = {
}
if (input_type === 'table') {
form_group += makeTable(setting, keypath, setting_value, isLocked)
form_group += makeTable(setting, keypath, setting_value)
} else {
if (input_type === 'select') {
form_group += "<select class='form-control' data-hidden-input='" + keypath + "'>'"
@ -97,12 +103,10 @@ var viewHelpers = {
if (setting.href) {
form_group += "<a href='" + setting.href + "'style='display: block;' role='button'"
+ (isLocked ? " disabled" : "")
+ common_attrs("btn " + setting.classes) + " target='_blank'>"
+ setting.button_label + "</a>";
} else {
form_group += "<button " + common_attrs("btn " + setting.classes)
+ (isLocked ? " disabled" : "") + ">"
form_group += "<button " + common_attrs("btn " + setting.classes) + ">"
+ setting.button_label + "</button>";
}
@ -114,7 +118,7 @@ var viewHelpers = {
form_group += "<input type='" + input_type + "'" + common_attrs() +
"placeholder='" + (_.has(setting, 'placeholder') ? setting.placeholder : "") +
"' value='" + setting_value + "'" + (isLocked ? " disabled" : "") + "/>"
"' value='" + setting_value + "'/>"
}
form_group += "<span class='help-block'>" + setting.help + "</span>"
@ -162,19 +166,31 @@ $(document).ready(function(){
});
$('#' + Settings.FORM_ID).on('click', '.' + Settings.ADD_ROW_BUTTON_CLASS, function(){
addTableRow(this);
addTableRow($(this).closest('tr'));
});
$('#' + Settings.FORM_ID).on('click', '.' + Settings.DEL_ROW_BUTTON_CLASS, function(){
deleteTableRow(this);
deleteTableRow($(this).closest('tr'));
});
$('#' + Settings.FORM_ID).on('click', '.' + Settings.ADD_CATEGORY_BUTTON_CLASS, function(){
addTableCategory($(this).closest('tr'));
});
$('#' + Settings.FORM_ID).on('click', '.' + Settings.DEL_CATEGORY_BUTTON_CLASS, function(){
deleteTableCategory($(this).closest('tr'));
});
$('#' + Settings.FORM_ID).on('click', '.' + Settings.TOGGLE_CATEGORY_COLUMN_CLASS, function(){
toggleTableCategory($(this).closest('tr'));
});
$('#' + Settings.FORM_ID).on('click', '.' + Settings.MOVE_UP_BUTTON_CLASS, function(){
moveTableRow(this, true);
moveTableRow($(this).closest('tr'), true);
});
$('#' + Settings.FORM_ID).on('click', '.' + Settings.MOVE_DOWN_BUTTON_CLASS, function(){
moveTableRow(this, false);
moveTableRow($(this).closest('tr'), false);
});
$('#' + Settings.FORM_ID).on('keyup', function(e){
@ -196,10 +212,11 @@ $(document).ready(function(){
}
if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click()
sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click();
sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click();
// set focus to the first input in the new row
$target.closest('table').find('tr.inputs input:first').focus()
$target.closest('table').find('tr.inputs input:first').focus();
}
}
@ -436,10 +453,8 @@ function setupHFAccountButton() {
$("[data-keypath='metaverse.automatic_networking']").hide();
}
var tokenLocked = _(Settings.data).valueForKeyPath("locked.metaverse.access_token");
// use the existing getFormGroup helper to ask for a button
var buttonGroup = viewHelpers.getFormGroup('', buttonSetting, Settings.data.values, false, tokenLocked);
var buttonGroup = viewHelpers.getFormGroup('', buttonSetting, Settings.data.values, false);
// add the button group to the top of the metaverse panel
$('#metaverse .panel-body').prepend(buttonGroup);
@ -629,7 +644,7 @@ function setupPlacesTable() {
label: 'Places',
html_id: Settings.PLACES_TABLE_ID,
help: "The following places currently point to this domain.</br>To point places to this domain, "
+ " go to the <a href='https://metaverse.highfidelity.com/user/places'>My Places</a> "
+ " go to the <a href='" + Settings.METAVERSE_URL + "/user/places'>My Places</a> "
+ "page in your High Fidelity Metaverse account.",
read_only: true,
columns: [
@ -650,7 +665,7 @@ function setupPlacesTable() {
}
// get a table for the places
var placesTableGroup = viewHelpers.getFormGroup('', placesTableSetting, Settings.data.values, false, false);
var placesTableGroup = viewHelpers.getFormGroup('', placesTableSetting, Settings.data.values, false);
// append the places table in the right place
$('#places_paths .panel-body').prepend(placesTableGroup);
@ -771,7 +786,7 @@ function chooseFromHighFidelityDomains(clickedButton) {
modal_buttons["success"] = {
label: 'Create new domain',
callback: function() {
window.open("https://metaverse.highfidelity.com/user/domains", '_blank');
window.open(Settings.METAVERSE_URL + "/user/domains", '_blank');
}
}
modal_body = "<p>You do not have any domains in your High Fidelity account." +
@ -850,10 +865,8 @@ function reloadSettings(callback) {
Settings.data = data;
Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true);
if (!_.has(data["locked"], "metaverse") && !_.has(data["locked"]["metaverse"], "id")) {
// append the domain selection modal, as long as it's not locked
appendDomainIDButtons();
}
// append the domain selection modal
appendDomainIDButtons();
// call our method to setup the HF account button
setupHFAccountButton();
@ -866,12 +879,6 @@ function reloadSettings(callback) {
$('[data-toggle="tooltip"]').tooltip();
// add tooltip to locked settings
$('label.locked').tooltip({
placement: 'right',
title: 'This setting is in the master config file and cannot be changed'
});
// call the callback now that settings are loaded
callback(true);
}).fail(function() {
@ -920,9 +927,10 @@ $('body').on('click', '.save-button', function(e){
return false;
});
function makeTable(setting, keypath, setting_value, isLocked) {
function makeTable(setting, keypath, setting_value) {
var isArray = !_.has(setting, 'key');
var isHash = !isArray;
var categoryKey = setting.categorize_by_key;
var isCategorized = !!categoryKey && isArray;
if (!isArray && setting.can_order) {
setting.can_order = false;
@ -937,9 +945,10 @@ function makeTable(setting, keypath, setting_value, isLocked) {
var nonDeletableRowKey = setting["non-deletable-row-key"];
var nonDeletableRowValues = setting["non-deletable-row-values"];
html += "<table class='table table-bordered " + (isLocked ? "locked-table" : "") + "' data-short-name='" + setting.name
+ "' name='" + keypath + "' id='" + (typeof setting.html_id !== 'undefined' ? setting.html_id : keypath)
+ "' data-setting-type='" + (isArray ? 'array' : 'hash') + "'>";
html += "<table class='table table-bordered' " +
"data-short-name='" + setting.name + "' name='" + keypath + "' " +
"id='" + (!_.isUndefined(setting.html_id) ? setting.html_id : keypath) + "' " +
"data-setting-type='" + (isArray ? 'array' : 'hash') + "'>";
if (setting.caption) {
html += "<caption>" + setting.caption + "</caption>"
@ -951,7 +960,7 @@ function makeTable(setting, keypath, setting_value, isLocked) {
_.each(setting.groups, function (group) {
html += "<td colspan='" + group.span + "'><strong>" + group.label + "</strong></td>"
})
if (!isLocked && !setting.read_only) {
if (!setting.read_only) {
if (setting.can_order) {
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='glyphicon glyphicon-sort'></a></td>";
@ -972,24 +981,43 @@ function makeTable(setting, keypath, setting_value, isLocked) {
html += "<td class='key'><strong>" + setting.key.label + "</strong></td>" // Key
}
var numVisibleColumns = 0;
_.each(setting.columns, function(col) {
html += "<td class='data " + (col.class ? col.class : '') + "'><strong>" + col.label + "</strong></td>" // Data
if (!col.hidden) numVisibleColumns++;
html += "<td " + (col.hidden ? "style='display: none;'" : "") + "class='data " +
(col.class ? col.class : '') + "'><strong>" + col.label + "</strong></td>" // Data
})
if (!isLocked && !setting.read_only) {
if (!setting.read_only) {
if (setting.can_order) {
numVisibleColumns++;
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='glyphicon glyphicon-sort'></a></td>";
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES + "'></td></tr>"
numVisibleColumns++;
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES + "'></td></tr>";
}
// populate rows in the table from existing values
var row_num = 1;
if (keypath.length > 0 && _.size(setting_value) > 0) {
var rowIsObject = setting.columns.length > 1;
_.each(setting_value, function(row, rowIndexOrName) {
html += "<tr class='" + Settings.DATA_ROW_CLASS + "'" + (isArray ? "" : "name='" + keypath + "." + rowIndexOrName + "'") + ">"
var categoryPair = {};
var categoryValue = "";
if (isCategorized) {
categoryValue = rowIsObject ? row[categoryKey] : row;
categoryPair[categoryKey] = categoryValue;
if (_.findIndex(setting_value, categoryPair) === rowIndexOrName) {
html += makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, setting.can_add_new_categories, "");
}
}
html += "<tr class='" + Settings.DATA_ROW_CLASS + "' " +
(isCategorized ? ("data-category='" + categoryValue + "'") : "") + " " +
(isArray ? "" : "name='" + keypath + "." + rowIndexOrName + "'") + ">";
if (setting.numbered === true) {
html += "<td class='numbered'>" + row_num + "</td>"
@ -1003,8 +1031,8 @@ function makeTable(setting, keypath, setting_value, isLocked) {
_.each(setting.columns, function(col) {
var colValue, colName;
if (isArray) {
rowIsObject = setting.columns.length > 1;
colValue = rowIsObject ? row[col.name] : row;
colName = keypath + "[" + rowIndexOrName + "]" + (rowIsObject ? "." + col.name : "");
} else {
@ -1016,22 +1044,30 @@ function makeTable(setting, keypath, setting_value, isLocked) {
|| (nonDeletableRowKey === col.name && nonDeletableRowValues.indexOf(colValue) !== -1);
if (isArray && col.type === "checkbox" && col.editable) {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
+ "<input type='checkbox' class='form-control table-checkbox' "
+ "name='" + colName + "'" + (colValue ? " checked" : "") + " /></td>";
html +=
"<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>" +
"<input type='checkbox' class='form-control table-checkbox' " +
"name='" + colName + "'" + (colValue ? " checked" : "") + "/>" +
"</td>";
} else if (isArray && col.type === "time" && col.editable) {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
+ "<input type='time' class='form-control table-time' "
+ "name='" + colName + "' value='" + (colValue || col.default || "00:00") + "' /></td>";
html +=
"<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>" +
"<input type='time' class='form-control table-time' name='" + colName + "' " +
"value='" + (colValue || col.default || "00:00") + "'/>" +
"</td>";
} else {
// Use a hidden input so that the values are posted.
html += "<td class='" + Settings.DATA_COL_CLASS + "' name='" + colName + "'>"
+ colValue + "<input type='hidden' name='" + colName + "' value='" + colValue + "'/></td>";
html +=
"<td class='" + Settings.DATA_COL_CLASS + "' " + (col.hidden ? "style='display: none;'" : "") +
"name='" + colName + "'>" +
colValue +
"<input type='hidden' name='" + colName + "' value='" + colValue + "'/>" +
"</td>";
}
})
});
if (!isLocked && !setting.read_only) {
if (!setting.read_only) {
if (setting.can_order) {
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES+
"'><a href='javascript:void(0);' class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a>"
@ -1047,24 +1083,53 @@ function makeTable(setting, keypath, setting_value, isLocked) {
html += "</tr>"
if (isCategorized && setting.can_add_new_rows && _.findLastIndex(setting_value, categoryPair) === rowIndexOrName) {
html += makeTableInputs(setting, categoryPair, categoryValue);
}
row_num++
});
}
// populate inputs in the table for new values
if (!isLocked && !setting.read_only && setting.can_add_new_rows) {
html += makeTableInputs(setting)
if (!setting.read_only) {
if (setting.can_add_new_categories) {
html += makeTableCategoryInput(setting, numVisibleColumns);
}
if (setting.can_add_new_rows || setting.can_add_new_categories) {
html += makeTableInputs(setting, {}, "");
}
}
html += "</table>"
return html;
}
function makeTableInputs(setting) {
var html = "<tr class='inputs'>"
function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, canRemove, message) {
var html =
"<tr class='" + Settings.DATA_CATEGORY_CLASS + "' data-key='" + categoryKey + "' data-category='" + categoryValue + "'>" +
"<td colspan='" + (numVisibleColumns - 1) + "' class='" + Settings.TOGGLE_CATEGORY_COLUMN_CLASS + "'>" +
"<span class='" + Settings.TOGGLE_CATEGORY_SPAN_CLASSES + " " + Settings.TOGGLE_CATEGORY_EXPANDED_CLASS + "'></span>" +
"<span message='" + message + "'>" + categoryValue + "</span>" +
"</td>" +
((canRemove) ? (
"<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES + "'>" +
"<a href='javascript:void(0);' class='" + Settings.DEL_CATEGORY_SPAN_CLASSES + "'></a>" +
"</td>"
) : (
"<td></td>"
)) +
"</tr>";
return html;
}
function makeTableInputs(setting, initialValues, categoryValue) {
var html = "<tr class='inputs'" + (setting.can_add_new_categories && !categoryValue ? " hidden" : "") + " " +
(categoryValue ? ("data-category='" + categoryValue + "'") : "") + " " +
(setting.categorize_by_key ? ("data-keep-field='" + setting.categorize_by_key + "'") : "") + ">";
if (setting.numbered === true) {
html += "<td class='numbered'></td>"
html += "<td class='numbered'></td>";
}
if (setting.key) {
@ -1074,15 +1139,21 @@ function makeTableInputs(setting) {
}
_.each(setting.columns, function(col) {
var defaultValue = _.has(initialValues, col.name) ? initialValues[col.name] : col.default;
if (col.type === "checkbox") {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
+ "<input type='checkbox' class='form-control table-checkbox' "
+ "name='" + col.name + "'" + (col.default ? " checked" : "") + "/></td>";
html +=
"<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>" +
"<input type='checkbox' class='form-control table-checkbox' " +
"name='" + col.name + "'" + (defaultValue ? " checked" : "") + "/>" +
"</td>";
} else {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>\
<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "'\
value='" + (col.default ? col.default : "") + "' data-default='" + (col.default ? col.default : "") + "'>\
</td>"
html +=
"<td " + (col.hidden ? "style='display: none;'" : "") + " class='" + Settings.DATA_COL_CLASS + "' " +
"name='" + col.name + "'>" +
"<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "' " +
"value='" + (defaultValue || "") + "' data-default='" + (defaultValue || "") + "'" +
(col.readonly ? " readonly" : "") + ">" +
"</td>";
}
})
@ -1090,12 +1161,30 @@ function makeTableInputs(setting) {
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'></td>"
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='glyphicon glyphicon-plus " + Settings.ADD_ROW_BUTTON_CLASS + "'></a></td>"
"'><a href='javascript:void(0);' class='" + Settings.ADD_ROW_SPAN_CLASSES + "'></a></td>"
html += "</tr>"
return html
}
function makeTableCategoryInput(setting, numVisibleColumns) {
var canAddRows = setting.can_add_new_rows;
var categoryKey = setting.categorize_by_key;
var placeholder = setting.new_category_placeholder || "";
var message = setting.new_category_message || "";
var html =
"<tr class='" + Settings.DATA_CATEGORY_CLASS + " inputs' data-can-add-rows='" + canAddRows + "' " +
"data-key='" + categoryKey + "' data-message='" + message + "'>" +
"<td colspan='" + (numVisibleColumns - 1) + "'>" +
"<input type='text' class='form-control' placeholder='" + placeholder + "'/>" +
"</td>" +
"<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES + "'>" +
"<a href='javascript:void(0);' class='" + Settings.ADD_CATEGORY_SPAN_CLASSES + "'></a>" +
"</td>" +
"</tr>";
return html;
}
function badgeSidebarForDifferences(changedElement) {
// figure out which group this input is in
var panelParentID = changedElement.closest('.panel').attr('id');
@ -1134,13 +1223,12 @@ function badgeSidebarForDifferences(changedElement) {
$("a[href='#" + panelParentID + "'] .badge").html(badgeValue);
}
function addTableRow(add_glyphicon) {
var row = $(add_glyphicon).closest('tr')
function addTableRow(row) {
var table = row.parents('table');
var isArray = table.data('setting-type') === 'array';
var keepField = row.data("keep-field");
var table = row.parents('table')
var isArray = table.data('setting-type') === 'array'
var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS)
var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS);
if (!isArray) {
// Check key spaces
@ -1257,10 +1345,12 @@ function addTableRow(add_glyphicon) {
} else {
console.log("Unknown table element")
}
})
});
input_clone.find('input').each(function(){
$(this).val($(this).attr('data-default'));
input_clone.children('td').each(function () {
if ($(this).attr("name") !== keepField) {
$(this).find("input").val($(this).attr('data-default'));
}
});
if (isArray) {
@ -1272,44 +1362,132 @@ function addTableRow(add_glyphicon) {
badgeSidebarForDifferences($(table))
row.parent().append(input_clone)
row.after(input_clone)
}
function deleteTableRow(delete_glyphicon) {
var row = $(delete_glyphicon).closest('tr')
function deleteTableRow($row) {
var $table = $row.closest('table');
var categoryName = $row.data("category");
var isArray = $table.data('setting-type') === 'array';
var table = $(row).closest('table')
var isArray = table.data('setting-type') === 'array'
row.empty();
$row.empty();
if (!isArray) {
row.html("<input type='hidden' class='form-control' name='"
+ row.attr('name') + "' data-changed='true' value=''>");
$row.html("<input type='hidden' class='form-control' name='" + $row.attr('name') + "' data-changed='true' value=''>");
} else {
if (table.find('.' + Settings.DATA_ROW_CLASS).length > 1) {
updateDataChangedForSiblingRows(row)
if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) {
// This is the last row of the category, so delete the header
$table.find('.' + Settings.DATA_CATEGORY_CLASS + "[data-category='" + categoryName + "']").remove();
}
if ($table.find('.' + Settings.DATA_ROW_CLASS).length > 1) {
updateDataChangedForSiblingRows($row);
// this isn't the last row - we can just remove it
row.remove()
$row.remove();
} else {
// this is the last row, we can't remove it completely since we need to post an empty array
row.removeClass(Settings.DATA_ROW_CLASS).removeClass(Settings.NEW_ROW_CLASS)
row.addClass('empty-array-row')
row.html("<input type='hidden' class='form-control' name='" + table.attr("name").replace('[]', '')
+ "' data-changed='true' value=''>");
$row
.removeClass(Settings.DATA_ROW_CLASS)
.removeClass(Settings.NEW_ROW_CLASS)
.removeAttr("data-category")
.addClass('empty-array-row')
.html("<input type='hidden' class='form-control' name='" + $table.attr("name").replace('[]', '') + "' " +
"data-changed='true' value=''>");
}
}
// we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated
badgeSidebarForDifferences($(table))
badgeSidebarForDifferences($table);
}
function moveTableRow(move_glyphicon, move_up) {
var row = $(move_glyphicon).closest('tr')
function addTableCategory($categoryInputRow) {
var $input = $categoryInputRow.find("input").first();
var categoryValue = $input.prop("value");
if (!categoryValue || $categoryInputRow.closest("table").find("tr[data-category='" + categoryValue + "']").length !== 0) {
$categoryInputRow.addClass("has-warning");
setTimeout(function () {
$categoryInputRow.removeClass("has-warning");
}, 400);
return;
}
var $rowInput = $categoryInputRow.next(".inputs").clone();
if (!$rowInput) {
console.error("Error cloning inputs");
}
var canAddRows = $categoryInputRow.data("can-add-rows");
var message = $categoryInputRow.data("message");
var categoryKey = $categoryInputRow.data("key");
var width = 0;
$categoryInputRow
.children("td")
.each(function () {
width += $(this).prop("colSpan") || 1;
});
$input
.prop("value", "")
.focus();
$rowInput.find("td[name='" + categoryKey + "'] > input").first()
.prop("value", categoryValue);
$rowInput
.attr("data-category", categoryValue)
.addClass(Settings.NEW_ROW_CLASS);
var $newCategoryRow = $(makeTableCategoryHeader(categoryKey, categoryValue, width, true, " - " + message));
$newCategoryRow.addClass(Settings.NEW_ROW_CLASS);
$categoryInputRow
.before($newCategoryRow)
.before($rowInput);
if (canAddRows) {
$rowInput.removeAttr("hidden");
} else {
addTableRow($rowInput);
}
}
function deleteTableCategory($categoryHeaderRow) {
var categoryName = $categoryHeaderRow.data("category");
$categoryHeaderRow
.closest("table")
.find("tr[data-category='" + categoryName + "']")
.each(function () {
if ($(this).hasClass(Settings.DATA_ROW_CLASS)) {
deleteTableRow($(this));
} else {
$(this).remove();
}
});
}
function toggleTableCategory($categoryHeaderRow) {
var $icon = $categoryHeaderRow.find("." + Settings.TOGGLE_CATEGORY_SPAN_CLASS).first();
var categoryName = $categoryHeaderRow.data("category");
var wasExpanded = $icon.hasClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS);
if (wasExpanded) {
$icon
.addClass(Settings.TOGGLE_CATEGORY_CONTRACTED_CLASS)
.removeClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS);
} else {
$icon
.addClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS)
.removeClass(Settings.TOGGLE_CATEGORY_CONTRACTED_CLASS);
}
$categoryHeaderRow
.closest("table")
.find("tr[data-category='" + categoryName + "']")
.toggleClass("contracted", wasExpanded);
}
function moveTableRow(row, move_up) {
var table = $(row).closest('table')
var isArray = table.data('setting-type') === 'array'
if (!isArray) {

View file

@ -120,6 +120,102 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
}
}
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress) {
NodePermissions userPerms;
userPerms.setAll(false);
if (isLocalUser) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: is local user, so:" << userPerms;
#endif
}
if (verifiedUsername.isEmpty()) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms;
#endif
if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
// this user comes from an IP we have in our permissions table, apply those permissions
userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: specific IP matches, so:" << userPerms;
#endif
}
} else {
if (_server->_settingsManager.havePermissionsForName(verifiedUsername)) {
userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: specific user matches, so:" << userPerms;
#endif
} else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
// this user comes from an IP we have in our permissions table, apply those permissions
userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: specific IP matches, so:" << userPerms;
#endif
} else {
// they are logged into metaverse, but we don't have specific permissions for them.
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: user is logged-into metaverse, so:" << userPerms;
#endif
// if this user is a friend of the domain-owner, give them friend's permissions
if (_domainOwnerFriends.contains(verifiedUsername)) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameFriends);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: user is friends with domain-owner, so:" << userPerms;
#endif
}
// if this user is a known member of a group, give them the implied permissions
foreach (QUuid groupID, _server->_settingsManager.getGroupIDs()) {
QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID);
if (rankID != QUuid()) {
userPerms |= _server->_settingsManager.getPermissionsForGroup(groupID, rankID);
GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: user " << verifiedUsername << "is in group:" << groupID << " rank:"
<< rank.name << "so:" << userPerms;
#endif
}
}
// if this user is a known member of a blacklist group, remove the implied permissions
foreach (QUuid groupID, _server->_settingsManager.getBlacklistGroupIDs()) {
QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID);
if (rankID != QUuid()) {
QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID);
if (rankID != QUuid()) {
userPerms &= ~_server->_settingsManager.getForbiddensForGroup(groupID, rankID);
GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: user is in blacklist group:" << groupID << " rank:" << rank.name
<< "so:" << userPerms;
#endif
}
}
}
}
userPerms.setID(verifiedUsername);
userPerms.setVerifiedUserName(verifiedUsername);
}
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: final:" << userPerms;
#endif
return userPerms;
}
void DomainGatekeeper::updateNodePermissions() {
// If the permissions were changed on the domain-server webpage (and nothing else was), a restart isn't required --
// we reprocess the permissions map and update the nodes here. The node list is frequently sent out to all
@ -129,40 +225,34 @@ void DomainGatekeeper::updateNodePermissions() {
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
limitedNodeList->eachNode([this, limitedNodeList, &nodesToKill](const SharedNodePointer& node){
QString username = node->getPermissions().getUserName();
NodePermissions userPerms(username);
// the id and the username in NodePermissions will often be the same, but id is set before
// authentication and verifiedUsername is only set once they user's key has been confirmed.
QString verifiedUsername = node->getPermissions().getVerifiedUserName();
NodePermissions userPerms(NodePermissionsKey(verifiedUsername, 0));
if (node->getPermissions().isAssignment) {
// this node is an assignment-client
userPerms.isAssignment = true;
userPerms.canAdjustLocks = true;
userPerms.canRezPermanentEntities = true;
userPerms.canRezTemporaryEntities = true;
userPerms.permissions |= NodePermissions::Permission::canConnectToDomain;
userPerms.permissions |= NodePermissions::Permission::canAdjustLocks;
userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities;
userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities;
} else {
// this node is an agent
userPerms.setAll(false);
const QHostAddress& addr = node->getLocalSocket().getAddress();
bool isLocalUser = (addr == limitedNodeList->getLocalSockAddr().getAddress() ||
addr == QHostAddress::LocalHost);
if (isLocalUser) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);
}
if (username.isEmpty()) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);
} else {
if (_server->_settingsManager.havePermissionsForName(username)) {
userPerms = _server->_settingsManager.getPermissionsForName(username);
} else {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);
}
}
// at this point we don't have a sending socket for packets from this node - assume it is the active socket
// or the public socket if we haven't activated a socket for the node yet
HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket();
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress());
}
node->setPermissions(userPerms);
if (!userPerms.canConnectToDomain) {
if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) {
qDebug() << "node" << node->getUUID() << "no longer has permission to connect.";
// hang up on this node
nodesToKill << node;
@ -215,12 +305,13 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
// cleanup the PendingAssignedNodeData for this assignment now that it's connecting
_pendingAssignedNodes.erase(it);
// always allow assignment clients to create and destroy entities
NodePermissions userPerms;
userPerms.isAssignment = true;
userPerms.canAdjustLocks = true;
userPerms.canRezPermanentEntities = true;
userPerms.canRezTemporaryEntities = true;
userPerms.permissions |= NodePermissions::Permission::canConnectToDomain;
// always allow assignment clients to create and destroy entities
userPerms.permissions |= NodePermissions::Permission::canAdjustLocks;
userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities;
userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities;
newNode->setPermissions(userPerms);
return newNode;
}
@ -234,64 +325,58 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
// start with empty permissions
NodePermissions userPerms(username);
NodePermissions userPerms(NodePermissionsKey(username, 0));
userPerms.setAll(false);
// check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection
QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress();
bool isLocalUser =
(senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost);
if (isLocalUser) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);
qDebug() << "user-permissions: is local user, so:" << userPerms;
}
if (!username.isEmpty() && usernameSignature.isEmpty()) {
// user is attempting to prove their identity to us, but we don't have enough information
sendConnectionTokenPacket(username, nodeConnection.senderSockAddr);
// ask for their public key right now to make sure we have it
requestUserPublicKey(username);
if (!userPerms.canConnectToDomain) {
QString verifiedUsername; // if this remains empty, consider this an anonymous connection attempt
if (!username.isEmpty()) {
if (usernameSignature.isEmpty()) {
// user is attempting to prove their identity to us, but we don't have enough information
sendConnectionTokenPacket(username, nodeConnection.senderSockAddr);
// ask for their public key right now to make sure we have it
requestUserPublicKey(username);
getGroupMemberships(username); // optimistically get started on group memberships
#ifdef WANT_DEBUG
qDebug() << "stalling login because we have no username-signature:" << username;
#endif
return SharedNodePointer();
}
}
if (username.isEmpty()) {
// they didn't tell us who they are
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);
qDebug() << "user-permissions: no username, so:" << userPerms;
} else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) {
// they are sent us a username and the signature verifies it
if (_server->_settingsManager.havePermissionsForName(username)) {
// we have specific permissions for this user.
userPerms = _server->_settingsManager.getPermissionsForName(username);
qDebug() << "user-permissions: specific user matches, so:" << userPerms;
} else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) {
// they sent us a username and the signature verifies it
getGroupMemberships(username);
verifiedUsername = username;
} else {
// they are logged into metaverse, but we don't have specific permissions for them.
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);
qDebug() << "user-permissions: user is logged in, so:" << userPerms;
}
userPerms.setUserName(username);
} else {
// they sent us a username, but it didn't check out
requestUserPublicKey(username);
if (!userPerms.canConnectToDomain) {
// they sent us a username, but it didn't check out
requestUserPublicKey(username);
#ifdef WANT_DEBUG
qDebug() << "stalling login because signature verification failed:" << username;
#endif
return SharedNodePointer();
}
}
qDebug() << "user-permissions: final:" << userPerms;
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress());
if (!userPerms.canConnectToDomain) {
if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) {
sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.",
nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::TooManyUsers);
#ifdef WANT_DEBUG
qDebug() << "stalling login due to permissions:" << username;
#endif
return SharedNodePointer();
}
if (!userPerms.canConnectPastMaxCapacity && !isWithinMaxCapacity()) {
if (!userPerms.can(NodePermissions::Permission::canConnectPastMaxCapacity) && !isWithinMaxCapacity()) {
// we can't allow this user to connect because we are at max capacity
sendConnectionDeniedPacket("Too many connected users.", nodeConnection.senderSockAddr,
DomainHandler::ConnectionRefusedReason::TooManyUsers);
#ifdef WANT_DEBUG
qDebug() << "stalling login due to max capacity:" << username;
#endif
return SharedNodePointer();
}
@ -305,10 +390,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
// we have a node that already has these exact sockets - this occurs if a node
// is unable to connect to the domain
hintNodeID = node->getUUID();
return false;
}
return true;
});
@ -328,6 +411,10 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY,
uuidStringWithoutCurlyBraces(newNode->getUUID()), username);
#ifdef WANT_DEBUG
qDebug() << "accepting login:" << username;
#endif
return newNode;
}
@ -365,11 +452,11 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node
bool DomainGatekeeper::verifyUserSignature(const QString& username,
const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr) {
// it's possible this user can be allowed to connect, but we need to check their username signature
QByteArray publicKeyArray = _userPublicKeys.value(username);
auto lowerUsername = username.toLower();
QByteArray publicKeyArray = _userPublicKeys.value(lowerUsername);
const QUuid& connectionToken = _connectionTokenHash.value(username.toLower());
const QUuid& connectionToken = _connectionTokenHash.value(lowerUsername);
if (!publicKeyArray.isEmpty() && !connectionToken.isNull()) {
// if we do have a public key for the user, check for a signature match
@ -379,8 +466,8 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
// first load up the public key into an RSA struct
RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size());
QByteArray lowercaseUsername = username.toLower().toUtf8();
QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()),
QByteArray lowercaseUsernameUTF8 = lowerUsername.toUtf8();
QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsernameUTF8.append(connectionToken.toRfc4122()),
QCryptographicHash::Sha256);
if (rsaPublicKey) {
@ -471,10 +558,20 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) {
return;
}
QString lowerUsername = username.toLower();
if (_inFlightPublicKeyRequests.contains(lowerUsername)) {
// public-key request for this username is already flight, not rerequesting
return;
}
_inFlightPublicKeyRequests += lowerUsername;
// even if we have a public key for them right now, request a new one in case it has just changed
JSONCallbackParameters callbackParams;
callbackParams.jsonCallbackReceiver = this;
callbackParams.jsonCallbackMethod = "publicKeyJSONCallback";
callbackParams.errorCallbackReceiver = this;
callbackParams.errorCallbackMethod = "publicKeyJSONErrorCallback";
const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key";
@ -485,28 +582,37 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) {
QNetworkAccessManager::GetOperation, callbackParams);
}
QString extractUsernameFromPublicKeyRequest(QNetworkReply& requestReply) {
// extract the username from the request url
QString username;
const QString PUBLIC_KEY_URL_REGEX_STRING = "api\\/v1\\/users\\/([A-Za-z0-9_\\.]+)\\/public_key";
QRegExp usernameRegex(PUBLIC_KEY_URL_REGEX_STRING);
if (usernameRegex.indexIn(requestReply.url().toString()) != -1) {
username = usernameRegex.cap(1);
}
return username.toLower();
}
void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) {
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
QString username = extractUsernameFromPublicKeyRequest(requestReply);
if (jsonObject["status"].toString() == "success") {
// figure out which user this is for
if (jsonObject["status"].toString() == "success" && !username.isEmpty()) {
// pull the public key as a QByteArray from this response
const QString JSON_DATA_KEY = "data";
const QString JSON_PUBLIC_KEY_KEY = "public_key";
const QString PUBLIC_KEY_URL_REGEX_STRING = "api\\/v1\\/users\\/([A-Za-z0-9_\\.]+)\\/public_key";
QRegExp usernameRegex(PUBLIC_KEY_URL_REGEX_STRING);
if (usernameRegex.indexIn(requestReply.url().toString()) != -1) {
QString username = usernameRegex.cap(1);
qDebug() << "Storing a public key for user" << username;
// pull the public key as a QByteArray from this response
const QString JSON_DATA_KEY = "data";
const QString JSON_PUBLIC_KEY_KEY = "public_key";
_userPublicKeys[username] =
QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8());
}
_userPublicKeys[username.toLower()] =
QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8());
}
_inFlightPublicKeyRequests.remove(username);
}
void DomainGatekeeper::publicKeyJSONErrorCallback(QNetworkReply& requestReply) {
qDebug() << "publicKey api call failed:" << requestReply.error();
QString username = extractUsernameFromPublicKeyRequest(requestReply);
_inFlightPublicKeyRequests.remove(username);
}
void DomainGatekeeper::sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr) {
@ -645,3 +751,159 @@ void DomainGatekeeper::processICEPingReplyPacket(QSharedPointer<ReceivedMessage>
sendingPeer->activateMatchingOrNewSymmetricSocket(message->getSenderSockAddr());
}
}
void DomainGatekeeper::getGroupMemberships(const QString& username) {
// loop through the groups mentioned on the settings page and ask if this user is in each. The replies
// will be received asynchronously and permissions will be updated as the answers come in.
// if we've already asked, wait for the answer before asking again
QString lowerUsername = username.toLower();
if (_inFlightGroupMembershipsRequests.contains(lowerUsername)) {
// public-key request for this username is already flight, not rerequesting
return;
}
_inFlightGroupMembershipsRequests += lowerUsername;
QJsonObject json;
QSet<QString> groupIDSet;
foreach (QUuid groupID, _server->_settingsManager.getGroupIDs() + _server->_settingsManager.getBlacklistGroupIDs()) {
groupIDSet += groupID.toString().mid(1,36);
}
QJsonArray groupIDs = QJsonArray::fromStringList(groupIDSet.toList());
json["groups"] = groupIDs;
JSONCallbackParameters callbackParams;
callbackParams.jsonCallbackReceiver = this;
callbackParams.jsonCallbackMethod = "getIsGroupMemberJSONCallback";
callbackParams.errorCallbackReceiver = this;
callbackParams.errorCallbackMethod = "getIsGroupMemberErrorCallback";
const QString GET_IS_GROUP_MEMBER_PATH = "api/v1/groups/members/%2";
DependencyManager::get<AccountManager>()->sendRequest(GET_IS_GROUP_MEMBER_PATH.arg(username),
AccountManagerAuth::Required,
QNetworkAccessManager::PostOperation, callbackParams,
QJsonDocument(json).toJson());
}
QString extractUsernameFromGroupMembershipsReply(QNetworkReply& requestReply) {
// extract the username from the request url
QString username;
const QString GROUP_MEMBERSHIPS_URL_REGEX_STRING = "api\\/v1\\/groups\\/members\\/([A-Za-z0-9_\\.]+)";
QRegExp usernameRegex(GROUP_MEMBERSHIPS_URL_REGEX_STRING);
if (usernameRegex.indexIn(requestReply.url().toString()) != -1) {
username = usernameRegex.cap(1);
}
return username.toLower();
}
void DomainGatekeeper::getIsGroupMemberJSONCallback(QNetworkReply& requestReply) {
// {
// "data":{
// "username":"sethalves",
// "groups":{
// "fd55479a-265d-4990-854e-3d04214ad1b0":{
// "name":"Blerg Blah",
// "rank":{
// "name":"admin",
// "order":1
// }
// }
// }
// },
// "status":"success"
// }
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
if (jsonObject["status"].toString() == "success") {
QJsonObject data = jsonObject["data"].toObject();
QJsonObject groups = data["groups"].toObject();
QString username = data["username"].toString();
_server->_settingsManager.clearGroupMemberships(username);
foreach (auto groupID, groups.keys()) {
QJsonObject group = groups[groupID].toObject();
QJsonObject rank = group["rank"].toObject();
QUuid rankID = QUuid(rank["id"].toString());
_server->_settingsManager.recordGroupMembership(username, groupID, rankID);
}
} else {
qDebug() << "getIsGroupMember api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact);
}
_inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply));
}
void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply& requestReply) {
qDebug() << "getIsGroupMember api call failed:" << requestReply.error();
_inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply));
}
void DomainGatekeeper::getDomainOwnerFriendsList() {
JSONCallbackParameters callbackParams;
callbackParams.jsonCallbackReceiver = this;
callbackParams.jsonCallbackMethod = "getDomainOwnerFriendsListJSONCallback";
callbackParams.errorCallbackReceiver = this;
callbackParams.errorCallbackMethod = "getDomainOwnerFriendsListErrorCallback";
const QString GET_FRIENDS_LIST_PATH = "api/v1/user/friends";
DependencyManager::get<AccountManager>()->sendRequest(GET_FRIENDS_LIST_PATH, AccountManagerAuth::Required,
QNetworkAccessManager::GetOperation, callbackParams, QByteArray(),
NULL, QVariantMap());
}
void DomainGatekeeper::getDomainOwnerFriendsListJSONCallback(QNetworkReply& requestReply) {
// {
// status: "success",
// data: {
// friends: [
// "chris",
// "freidrica",
// "G",
// "huffman",
// "leo",
// "philip",
// "ryan",
// "sam",
// "ZappoMan"
// ]
// }
// }
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
if (jsonObject["status"].toString() == "success") {
_domainOwnerFriends.clear();
QJsonArray friends = jsonObject["data"].toObject()["friends"].toArray();
for (int i = 0; i < friends.size(); i++) {
_domainOwnerFriends += friends.at(i).toString();
}
} else {
qDebug() << "getDomainOwnerFriendsList api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact);
}
}
void DomainGatekeeper::getDomainOwnerFriendsListErrorCallback(QNetworkReply& requestReply) {
qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply.error();
}
void DomainGatekeeper::refreshGroupsCache() {
// if agents are connected to this domain, refresh our cached information about groups and memberships in such.
getDomainOwnerFriendsList();
auto nodeList = DependencyManager::get<LimitedNodeList>();
nodeList->eachNode([&](const SharedNodePointer& node) {
if (!node->getPermissions().isAssignment) {
// this node is an agent
const QString& verifiedUserName = node->getPermissions().getVerifiedUserName();
if (!verifiedUserName.isEmpty()) {
getGroupMemberships(verifiedUserName);
}
}
});
_server->_settingsManager.apiRefreshGroupInformation();
updateNodePermissions();
#if WANT_DEBUG
_server->_settingsManager.debugDumpGroupsState();
#endif
}

View file

@ -51,7 +51,16 @@ public slots:
void processICEPeerInformationPacket(QSharedPointer<ReceivedMessage> message);
void publicKeyJSONCallback(QNetworkReply& requestReply);
void publicKeyJSONErrorCallback(QNetworkReply& requestReply);
void getIsGroupMemberJSONCallback(QNetworkReply& requestReply);
void getIsGroupMemberErrorCallback(QNetworkReply& requestReply);
void getDomainOwnerFriendsListJSONCallback(QNetworkReply& requestReply);
void getDomainOwnerFriendsListErrorCallback(QNetworkReply& requestReply);
void refreshGroupsCache();
signals:
void killNode(SharedNodePointer node);
void connectedNode(SharedNodePointer node);
@ -93,6 +102,15 @@ private:
QHash<QString, QUuid> _connectionTokenHash;
QHash<QString, QByteArray> _userPublicKeys;
QSet<QString> _inFlightPublicKeyRequests; // keep track of which we've already asked for
QSet<QString> _domainOwnerFriends; // keep track of friends of the domain owner
QSet<QString> _inFlightGroupMembershipsRequests; // keep track of which we've already asked for
NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress);
void getGroupMemberships(const QString& username);
// void getIsGroupMember(const QString& username, const QUuid groupID);
void getDomainOwnerFriendsList();
};

View file

@ -184,10 +184,10 @@ void DomainMetadata::securityChanged(bool send) {
QString restriction;
const auto& settingsManager = static_cast<DomainServer*>(parent())->_settingsManager;
bool hasAnonymousAccess =
settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous).canConnectToDomain;
bool hasHifiAccess =
settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn).canConnectToDomain;
bool hasAnonymousAccess = settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous).can(
NodePermissions::Permission::canConnectToDomain);
bool hasHifiAccess = settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn).can(
NodePermissions::Permission::canConnectToDomain);
if (hasAnonymousAccess) {
restriction = hasHifiAccess ? RESTRICTION_OPEN : RESTRICTION_ANON;
} else if (hasHifiAccess) {

View file

@ -38,6 +38,7 @@
#include <UUID.h>
#include <LogHandler.h>
#include <ServerPathUtils.h>
#include <NumericalConstants.h>
#include "DomainServerNodeData.h"
#include "NodeConnectionData.h"
@ -106,15 +107,28 @@ DomainServer::DomainServer(int argc, char* argv[]) :
connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions,
&_gatekeeper, &DomainGatekeeper::updateNodePermissions);
setupGroupCacheRefresh();
// if we were given a certificate/private key or oauth credentials they must succeed
if (!(optionallyReadX509KeyAndCertificate() && optionallySetupOAuth())) {
return;
}
_settingsManager.apiRefreshGroupInformation();
setupNodeListAndAssignments();
if (_type == MetaverseDomain) {
// if we have a metaverse domain, we'll need an access token to heartbeat handle auto-networking
resetAccountManagerAccessToken();
}
setupAutomaticNetworking();
if (!getID().isNull()) {
if (!getID().isNull() && _type != NonMetaverse) {
// setup periodic heartbeats to metaverse API
setupHeartbeatToMetaverse();
// send the first heartbeat immediately
sendHeartbeatToMetaverse();
}
@ -296,16 +310,22 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) {
// store the new ID and auto networking setting on disk
_settingsManager.persistToFile();
// change our domain ID immediately
DependencyManager::get<LimitedNodeList>()->setSessionUUID(QUuid { id });
// store the new token to the account info
auto accountManager = DependencyManager::get<AccountManager>();
accountManager->setTemporaryDomain(id, key);
// change our domain ID immediately
DependencyManager::get<LimitedNodeList>()->setSessionUUID(QUuid { id });
// change our type to reflect that we are a temporary domain now
_type = MetaverseTemporaryDomain;
// update our heartbeats to use the correct id
setupICEHeartbeatForFullNetworking();
setupHeartbeatToMetaverse();
// if we have a current ICE server address, update it in the API for the new temporary domain
sendICEServerAddressToMetaverseAPI();
} else {
qWarning() << "There were problems parsing the API response containing a temporary domain name. Please try again"
<< "via domain-server relaunch or from the domain-server settings.";
@ -389,6 +409,16 @@ void DomainServer::setupNodeListAndAssignments() {
const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH);
if (idValueVariant) {
nodeList->setSessionUUID(idValueVariant->toString());
// if we have an ID, we'll assume we're a metaverse domain
// now see if we think we're a temp domain (we have an API key) or a full domain
const auto& temporaryDomainKey = DependencyManager::get<AccountManager>()->getTemporaryDomainKey(getID());
if (temporaryDomainKey.isEmpty()) {
_type = MetaverseDomain;
} else {
_type = MetaverseTemporaryDomain;
}
} else {
nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID
}
@ -406,6 +436,7 @@ void DomainServer::setupNodeListAndAssignments() {
// NodeList won't be available to the settings manager when it is created, so call registerListener here
packetReceiver.registerListener(PacketType::DomainSettingsRequest, &_settingsManager, "processSettingsRequestPacket");
packetReceiver.registerListener(PacketType::NodeKickRequest, &_settingsManager, "processNodeKickRequestPacket");
// register the gatekeeper for the packets it needs to receive
packetReceiver.registerListener(PacketType::DomainConnectRequest, &_gatekeeper, "processConnectRequestPacket");
@ -471,42 +502,46 @@ bool DomainServer::resetAccountManagerAccessToken() {
}
void DomainServer::setupAutomaticNetworking() {
qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting;
resetAccountManagerAccessToken();
_automaticNetworkingSetting =
_settingsManager.valueOrDefaultValueForKeyPath(METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH).toString();
auto nodeList = DependencyManager::get<LimitedNodeList>();
const QUuid& domainID = getID();
qDebug() << "Configuring automatic networking in domain-server as" << _automaticNetworkingSetting;
if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) {
setupICEHeartbeatForFullNetworking();
}
if (_automaticNetworkingSetting != DISABLED_AUTOMATIC_NETWORKING_VALUE) {
const QUuid& domainID = getID();
if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE ||
_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) {
if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) {
setupICEHeartbeatForFullNetworking();
}
if (!domainID.isNull()) {
qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID"
<< uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString();
if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE ||
_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) {
if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE) {
// send any public socket changes to the data server so nodes can find us at our new IP
connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged,
this, &DomainServer::performIPAddressUpdate);
if (!domainID.isNull()) {
qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID"
<< uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString();
// have the LNL enable public socket updating via STUN
nodeList->startSTUNPublicSocketUpdate();
if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE) {
auto nodeList = DependencyManager::get<LimitedNodeList>();
// send any public socket changes to the data server so nodes can find us at our new IP
connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged,
this, &DomainServer::performIPAddressUpdate);
// have the LNL enable public socket updating via STUN
nodeList->startSTUNPublicSocketUpdate();
}
} else {
qDebug() << "Cannot enable domain-server automatic networking without a domain ID."
<< "Please add an ID to your config file or via the web interface.";
return;
}
} else {
qDebug() << "Cannot enable domain-server automatic networking without a domain ID."
<< "Please add an ID to your config file or via the web interface.";
return;
}
}
}
void DomainServer::setupHeartbeatToMetaverse() {
@ -1098,12 +1133,11 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) {
static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking";
domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting;
// add access level for anonymous connections
// consider the domain to be "restricted" if anonymous connections are disallowed
static const QString RESTRICTED_ACCESS_FLAG = "restricted";
NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous);
domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain;
domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.can(NodePermissions::Permission::canConnectToDomain);
const auto& temporaryDomainKey = DependencyManager::get<AccountManager>()->getTemporaryDomainKey(getID());
if (!temporaryDomainKey.isEmpty()) {
@ -1134,42 +1168,45 @@ void DomainServer::handleMetaverseHeartbeatError(QNetworkReply& requestReply) {
return;
}
// check if we need to force a new temporary domain name
switch (requestReply.error()) {
// if we have a temporary domain with a bad token, we get a 401
case QNetworkReply::NetworkError::AuthenticationRequiredError: {
static const QString DATA_KEY = "data";
static const QString TOKEN_KEY = "api_key";
// only attempt to grab a new temporary name if we're already a temporary domain server
if (_type == MetaverseTemporaryDomain) {
// check if we need to force a new temporary domain name
switch (requestReply.error()) {
// if we have a temporary domain with a bad token, we get a 401
case QNetworkReply::NetworkError::AuthenticationRequiredError: {
static const QString DATA_KEY = "data";
static const QString TOKEN_KEY = "api_key";
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY];
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY];
if (!tokenFailure.isNull()) {
qWarning() << "Temporary domain name lacks a valid API key, and is being reset.";
if (!tokenFailure.isNull()) {
qWarning() << "Temporary domain name lacks a valid API key, and is being reset.";
}
break;
}
break;
// if the domain does not (or no longer) exists, we get a 404
case QNetworkReply::NetworkError::ContentNotFoundError:
qWarning() << "Domain not found, getting a new temporary domain.";
break;
// otherwise, we erred on something else, and should not force a temporary domain
default:
return;
}
// if the domain does not (or no longer) exists, we get a 404
case QNetworkReply::NetworkError::ContentNotFoundError:
qWarning() << "Domain not found, getting a new temporary domain.";
break;
// otherwise, we erred on something else, and should not force a temporary domain
default:
return;
}
// halt heartbeats until we have a token
_metaverseHeartbeatTimer->deleteLater();
_metaverseHeartbeatTimer = nullptr;
// halt heartbeats until we have a token
_metaverseHeartbeatTimer->deleteLater();
_metaverseHeartbeatTimer = nullptr;
// give up eventually to avoid flooding traffic
static const int MAX_ATTEMPTS = 5;
static int attempt = 0;
if (++attempt < MAX_ATTEMPTS) {
// get a new temporary name and token
getTemporaryName(true);
} else {
qWarning() << "Already attempted too many temporary domain requests. Please set a domain ID manually or restart.";
// give up eventually to avoid flooding traffic
static const int MAX_ATTEMPTS = 5;
static int attempt = 0;
if (++attempt < MAX_ATTEMPTS) {
// get a new temporary name and token
getTemporaryName(true);
} else {
qWarning() << "Already attempted too many temporary domain requests. Please set a domain ID manually or restart.";
}
}
}
@ -1196,7 +1233,10 @@ void DomainServer::sendICEServerAddressToMetaverseAPI() {
callbackParameters.errorCallbackReceiver = this;
callbackParameters.errorCallbackMethod = "handleFailedICEServerAddressUpdate";
qDebug() << "Updating ice-server address in High Fidelity Metaverse API to" << _iceServerSocket.getAddress().toString();
static QString repeatedMessage = LogHandler::getInstance().addOnlyOnceMessageRegex
("Updating ice-server address in High Fidelity Metaverse API to [^ \n]+");
qDebug() << "Updating ice-server address in High Fidelity Metaverse API to"
<< _iceServerSocket.getAddress().toString();
static const QString DOMAIN_ICE_ADDRESS_UPDATE = "/api/v1/domains/%1/ice_server_address";
@ -2327,3 +2367,14 @@ void DomainServer::randomizeICEServerAddress(bool shouldTriggerHostLookup) {
// immediately send an update to the metaverse API when our ice-server changes
sendICEServerAddressToMetaverseAPI();
}
void DomainServer::setupGroupCacheRefresh() {
const int REFRESH_GROUPS_INTERVAL_MSECS = 15 * MSECS_PER_SECOND;
if (!_metaverseGroupCacheTimer) {
// setup a timer to refresh this server's cached group details
_metaverseGroupCacheTimer = new QTimer { this };
connect(_metaverseGroupCacheTimer, &QTimer::timeout, &_gatekeeper, &DomainGatekeeper::refreshGroupsCache);
_metaverseGroupCacheTimer->start(REFRESH_GROUPS_INTERVAL_MSECS);
}
}

View file

@ -41,7 +41,13 @@ class DomainServer : public QCoreApplication, public HTTPSRequestHandler {
public:
DomainServer(int argc, char* argv[]);
~DomainServer();
enum DomainType {
NonMetaverse,
MetaverseDomain,
MetaverseTemporaryDomain
};
static int const EXIT_CODE_REBOOT;
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false);
@ -64,7 +70,7 @@ public slots:
void processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatACK(QSharedPointer<ReceivedMessage> message);
private slots:
void aboutToQuit();
@ -74,7 +80,7 @@ private slots:
void performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr);
void sendHeartbeatToMetaverse() { sendHeartbeatToMetaverse(QString()); }
void sendHeartbeatToIceServer();
void handleConnectedNode(SharedNodePointer newNode);
void handleTempDomainSuccess(QNetworkReply& requestReply);
@ -96,7 +102,7 @@ signals:
void iceServerChanged();
void userConnected();
void userDisconnected();
private:
const QUuid& getID();
@ -136,7 +142,7 @@ private:
SharedAssignmentPointer deployableAssignmentForRequest(const Assignment& requestAssignment);
void refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment);
void addStaticAssignmentsToQueue();
QUrl oauthRedirectURL();
QUrl oauthAuthorizationURL(const QUuid& stateUUID = QUuid::createUuid());
@ -151,7 +157,9 @@ private:
QJsonObject jsonForSocket(const HifiSockAddr& socket);
QJsonObject jsonObjectForNode(const SharedNodePointer& node);
void setupGroupCacheRefresh();
DomainGatekeeper _gatekeeper;
HTTPManager _httpManager;
@ -184,6 +192,7 @@ private:
DomainMetadata* _metadata { nullptr };
QTimer* _iceHeartbeatTimer { nullptr };
QTimer* _metaverseHeartbeatTimer { nullptr };
QTimer* _metaverseGroupCacheTimer { nullptr };
QList<QHostAddress> _iceServerAddresses;
QSet<QHostAddress> _failedIceServerAddresses;
@ -192,6 +201,8 @@ private:
int _numHeartbeatDenials { 0 };
bool _connectedToICEServer { false };
DomainType _type { DomainType::NonMetaverse };
friend class DomainGatekeeper;
friend class DomainMetadata;
};

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,12 @@ const QString SETTINGS_PATH = "/settings";
const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".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";
const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions";
const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens";
using GroupByUUIDKey = QPair<QUuid, QUuid>; // groupID, rankID
class DomainServerSettingsManager : public QObject {
Q_OBJECT
@ -38,23 +44,67 @@ public:
void setupConfigMap(const QStringList& argumentList);
QVariant valueOrDefaultValueForKeyPath(const QString& keyPath);
QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); }
QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); }
QVariantMap& getSettingsMap() { return _configMap.getConfig(); }
QVariantMap& getDescriptorsMap();
bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name); }
bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); }
NodePermissions getStandardPermissionsForName(const QString& name) const;
// these give access to anonymous/localhost/logged-in settings from the domain-server settings page
bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, 0); }
NodePermissions getStandardPermissionsForName(const NodePermissionsKey& name) const;
// these give access to permissions for specific user-names from the domain-server settings page
bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name, 0); }
NodePermissions getPermissionsForName(const QString& name) const;
QStringList getAllNames() { return _agentPermissions.keys(); }
NodePermissions getPermissionsForName(const NodePermissionsKey& key) const { return getPermissionsForName(key.first); }
QStringList getAllNames() const;
// these give access to permissions for specific IPs from the domain-server settings page
bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), 0); }
NodePermissions getPermissionsForIP(const QHostAddress& address) const;
// these give access to permissions for specific groups from the domain-server settings page
bool havePermissionsForGroup(const QString& groupName, QUuid rankID) const {
return _groupPermissions.contains(groupName, rankID);
}
NodePermissions getPermissionsForGroup(const QString& groupName, QUuid rankID) const;
NodePermissions getPermissionsForGroup(const QUuid& groupID, QUuid rankID) const;
// these remove permissions from users in certain groups
bool haveForbiddensForGroup(const QString& groupName, QUuid rankID) const {
return _groupForbiddens.contains(groupName, rankID);
}
NodePermissions getForbiddensForGroup(const QString& groupName, QUuid rankID) const;
NodePermissions getForbiddensForGroup(const QUuid& groupID, QUuid rankID) const;
QStringList getAllKnownGroupNames();
bool setGroupID(const QString& groupName, const QUuid& groupID);
GroupRank getGroupRank(QUuid groupID, QUuid rankID) { return _groupRanks[groupID][rankID]; }
QList<QUuid> getGroupIDs();
QList<QUuid> getBlacklistGroupIDs();
// these are used to locally cache the result of calling "api/v1/groups/.../is_member/..." on metaverse's api
void clearGroupMemberships(const QString& name) { _groupMembership[name.toLower()].clear(); }
void recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID);
QUuid isGroupMember(const QString& name, const QUuid& groupID); // returns rank or -1 if not a member
// calls http api to refresh group information
void apiRefreshGroupInformation();
void debugDumpGroupsState();
signals:
void updateNodePermissions();
public slots:
void apiGetGroupIDJSONCallback(QNetworkReply& requestReply);
void apiGetGroupIDErrorCallback(QNetworkReply& requestReply);
void apiGetGroupRanksJSONCallback(QNetworkReply& requestReply);
void apiGetGroupRanksErrorCallback(QNetworkReply& requestReply);
private slots:
void processSettingsRequestPacket(QSharedPointer<ReceivedMessage> message);
void processNodeKickRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
private:
QStringList _argumentList;
@ -76,11 +126,37 @@ private:
void validateDescriptorsMap();
void packPermissionsForMap(QString mapName, NodePermissionsMap& agentPermissions, QString keyPath);
// these cause calls to metaverse's group api
void apiGetGroupID(const QString& groupName);
void apiGetGroupRanks(const QUuid& groupID);
void initializeGroupPermissions(NodePermissionsMap& permissionsRows, QString groupName, NodePermissionsPointer perms);
void packPermissionsForMap(QString mapName, NodePermissionsMap& permissionsRows, QString keyPath);
void packPermissions();
void unpackPermissions();
NodePermissionsMap _standardAgentPermissions; // anonymous, logged-in, localhost
bool unpackPermissionsForKeypath(const QString& keyPath, NodePermissionsMap* destinationMapPointer,
std::function<void(NodePermissionsPointer)> customUnpacker = {});
bool ensurePermissionsForGroupRanks();
NodePermissionsMap _standardAgentPermissions; // anonymous, logged-in, localhost, friend-of-domain-owner
NodePermissionsMap _agentPermissions; // specific account-names
NodePermissionsMap _ipPermissions; // permissions granted by node IP address
NodePermissionsMap _groupPermissions; // permissions granted by membership to specific groups
NodePermissionsMap _groupForbiddens; // permissions denied due to membership in a specific group
// these are like _groupPermissions and _groupForbiddens but with uuids rather than group-names in the keys
QHash<GroupByUUIDKey, NodePermissionsPointer> _groupPermissionsByUUID;
QHash<GroupByUUIDKey, NodePermissionsPointer> _groupForbiddensByUUID;
QHash<QString, QUuid> _groupIDs; // keep track of group-name to group-id mappings
QHash<QUuid, QString> _groupNames; // keep track of group-id to group-name mappings
// remember the responses to api/v1/groups/%1/ranks
QHash<QUuid, QHash<QUuid, GroupRank>> _groupRanks; // QHash<group-id, QHash<rankID, rank>>
// keep track of answers to api queries about which users are in which groups
QHash<QString, QHash<QUuid, QUuid>> _groupMembership; // QHash<user-name, QHash<group-id, rank-id>>
};
#endif // hifi_DomainServerSettingsManager_h

View file

@ -1,15 +1,26 @@
{
"name": "Hydra to Standard",
"channels": [
{ "from": "Hydra.LY", "filters": "invert", "to": "Standard.LY" },
{ "from": "Hydra.LX", "to": "Standard.LX" },
{ "from": "Hydra.LY", "to": "Standard.LY",
"filters": [
{ "type": "deadZone", "min": 0.05 },
"invert"
]
},
{ "from": "Hydra.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.LX" },
{ "from": "Hydra.LT", "to": "Standard.LTClick",
"peek": true,
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
},
{ "from": "Hydra.LT", "to": "Standard.LT" },
{ "from": "Hydra.RY", "filters": "invert", "to": "Standard.RY" },
{ "from": "Hydra.RX", "to": "Standard.RX" },
{ "from": "Hydra.LT", "to": "Standard.LT" },
{ "from": "Hydra.RY", "to": "Standard.RY",
"filters": [
{ "type": "deadZone", "min": 0.05 },
"invert"
]
},
{ "from": "Hydra.RX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.RX" },
{ "from": "Hydra.RT", "to": "Standard.RTClick",
"peek": true,
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
@ -28,7 +39,6 @@
{ "from": [ "Hydra.R1", "Hydra.R3" ], "to": "Standard.RightPrimaryThumb" },
{ "from": [ "Hydra.R2", "Hydra.R4" ], "to": "Standard.RightSecondaryThumb" },
{ "from": [ "Hydra.L2", "Hydra.L4" ], "to": "Standard.LeftSecondaryThumb" },
{ "from": "Hydra.LeftHand", "to": "Standard.LeftHand" },
{ "from": "Hydra.RightHand", "to": "Standard.RightHand" }
]

View file

@ -63,6 +63,19 @@
["Keyboard.D", "Keyboard.Right", "Keyboard.TouchpadRight"]
]
},
"when": "Application.CameraFirstPerson",
"to": "Actions.Yaw"
},
{ "from": { "makeAxis" : [
["Keyboard.A", "Keyboard.Left", "Keyboard.TouchpadLeft"],
["Keyboard.D", "Keyboard.Right", "Keyboard.TouchpadRight"]
]
},
"when": "Application.CameraThirdPerson",
"to": "Actions.Yaw"
},
{ "from": { "makeAxis" : [ ["Keyboard.A"], ["Keyboard.D"] ] },
"when": "Application.CameraFSM",
"to": "Actions.Yaw"
},
@ -81,9 +94,10 @@
{ "from": "Keyboard.Right", "when": "Keyboard.Shift", "to": "Actions.LATERAL_RIGHT" },
{ "from": "Keyboard.Down", "when": "Keyboard.Shift", "to": "Actions.PITCH_DOWN" },
{ "from": "Keyboard.Up", "when": "Keyboard.Shift", "to": "Actions.PITCH_UP" },
{ "from": "Keyboard.Up", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.Down", "to": "Actions.LONGITUDINAL_BACKWARD" },
{ "from": "Keyboard.Up", "when": "Application.CameraFirstPerson", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.Up", "when": "Application.CameraThirdPerson", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.Down", "when": "Application.CameraFirstPerson", "to": "Actions.LONGITUDINAL_BACKWARD" },
{ "from": "Keyboard.Down", "when": "Application.CameraThirdPerson", "to": "Actions.LONGITUDINAL_BACKWARD" },
{ "from": "Keyboard.PgDown", "to": "Actions.VERTICAL_DOWN" },
{ "from": "Keyboard.PgUp", "to": "Actions.VERTICAL_UP" },

View file

@ -1,13 +1,21 @@
{
"name": "Oculus Touch to Standard",
"channels": [
{ "from": "OculusTouch.A", "to": "Standard.RightPrimaryThumb" },
{ "from": "OculusTouch.B", "to": "Standard.RightSecondaryThumb" },
{ "from": "OculusTouch.X", "to": "Standard.LeftPrimaryThumb" },
{ "from": "OculusTouch.Y", "to": "Standard.LeftSecondaryThumb" },
{ "from": "OculusTouch.A", "to": "Standard.RightPrimaryThumb", "peek": true },
{ "from": "OculusTouch.X", "to": "Standard.LeftPrimaryThumb", "peek": true },
{ "from": "OculusTouch.LY", "filters": "invert", "to": "Standard.LY" },
{ "from": "OculusTouch.LX", "to": "Standard.LX" },
{ "from": "OculusTouch.A", "to": "Standard.A" },
{ "from": "OculusTouch.B", "to": "Standard.B" },
{ "from": "OculusTouch.X", "to": "Standard.X" },
{ "from": "OculusTouch.Y", "to": "Standard.Y" },
{ "from": "OculusTouch.LY", "to": "Standard.LY",
"filters": [
{ "type": "deadZone", "min": 0.05 },
"invert"
]
},
{ "from": "OculusTouch.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.LX" },
{ "from": "OculusTouch.LT", "to": "Standard.LTClick",
"peek": true,
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
@ -17,8 +25,13 @@
{ "from": "OculusTouch.LeftGrip", "to": "Standard.LeftGrip" },
{ "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" },
{ "from": "OculusTouch.RY", "filters": "invert", "to": "Standard.RY" },
{ "from": "OculusTouch.RX", "to": "Standard.RX" },
{ "from": "OculusTouch.RY", "to": "Standard.RY",
"filters": [
{ "type": "deadZone", "min": 0.05 },
"invert"
]
},
{ "from": "OculusTouch.RX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.RX" },
{ "from": "OculusTouch.RT", "to": "Standard.RTClick",
"peek": true,
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]

View file

@ -32,9 +32,6 @@
{ "from": "Standard.Back", "to": "Actions.CycleCamera" },
{ "from": "Standard.Start", "to": "Actions.ContextMenu" },
{ "from": [ "Standard.DU", "Standard.DL", "Standard.DR", "Standard.DD" ], "to": "Standard.LeftPrimaryThumb" },
{ "from": [ "Standard.A", "Standard.B", "Standard.X", "Standard.Y" ], "to": "Standard.RightPrimaryThumb" },
{ "from": "Standard.LT", "to": "Actions.LeftHandClick" },
{ "from": "Standard.RT", "to": "Actions.RightHandClick" },

View file

@ -15,12 +15,14 @@
{ "from": "GamePad.Back", "to": "Standard.Back" },
{ "from": "GamePad.Start", "to": "Standard.Start" },
{ "from": [ "GamePad.DU", "GamePad.DL", "GamePad.DR", "GamePad.DD" ], "to": "Standard.LeftPrimaryThumb", "peek": true },
{ "from": "GamePad.DU", "to": "Standard.DU" },
{ "from": "GamePad.DD", "to": "Standard.DD" },
{ "from": "GamePad.DL", "to": "Standard.DL" },
{ "from": "GamePad.DR", "to": "Standard.DR" },
{ "from": [ "GamePad.A", "GamePad.B", "GamePad.X", "GamePad.Y" ], "to": "Standard.RightPrimaryThumb", "peek": true },
{ "from": "GamePad.A", "to": "Standard.A" },
{ "from": "GamePad.B", "to": "Standard.B" },
{ "from": "GamePad.X", "to": "Standard.X" },

Binary file not shown.

After

(image error) Size: 6.1 KiB

Binary file not shown.

After

(image error) Size: 394 KiB

View file

@ -314,6 +314,14 @@ ScrollingWindow {
});
}
Timer {
id: doUploadTimer
property var url
property bool isConnected: false
interval: 5
repeat: false
running: false
}
property var uploadOpen: false;
Timer {
@ -366,6 +374,10 @@ ScrollingWindow {
}, dropping);
}
function initiateUpload(url) {
doUpload(doUploadTimer.url, false);
}
if (fileUrl) {
doUpload(fileUrl, true);
} else {
@ -373,12 +385,21 @@ ScrollingWindow {
selectDirectory: false,
dir: currentDirectory
});
browser.canceled.connect(function() {
uploadOpen = false;
});
browser.selectedFile.connect(function(url) {
currentDirectory = browser.dir;
doUpload(url, false);
// Initiate upload from a timer so that file browser dialog can close beforehand.
doUploadTimer.url = url;
if (!doUploadTimer.isConnected) {
doUploadTimer.triggered.connect(function() { initiateUpload(); });
doUploadTimer.isConnected = true;
}
doUploadTimer.start();
});
}
}
@ -500,14 +521,15 @@ ScrollingWindow {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
var index = treeView.indexAt(mouse.x, mouse.y);
treeView.selection.setCurrentIndex(index, 0x0002);
contextMenu.currentIndex = index;
contextMenu.popup();
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();
}
}
}
}
HifiControls.ContentSection {
id: uploadSection

View file

@ -39,6 +39,23 @@ Windows.ScrollingWindow {
// missing signal
signal sendToScript(var message);
signal moved(vector2d position);
signal resized(size size);
function notifyMoved() {
moved(Qt.vector2d(x, y));
}
function notifyResized() {
resized(Qt.size(width, height));
}
onXChanged: notifyMoved();
onYChanged: notifyMoved();
onWidthChanged: notifyResized();
onHeightChanged: notifyResized();
Item {
width: pane.contentWidth
implicitHeight: pane.scrollHeight

View file

@ -35,7 +35,6 @@ WebEngineView {
}
onUrlChanged: {
console.log("Url changed to " + url);
var originalUrl = url.toString();
newUrl = urlHandler.fixupUrl(originalUrl).toString();
if (newUrl !== originalUrl) {

View file

@ -26,7 +26,6 @@ WebEngineView {
}
onUrlChanged: {
console.log("Url changed to " + url);
var originalUrl = url.toString();
newUrl = urlHandler.fixupUrl(originalUrl).toString();
if (newUrl !== originalUrl) {

View file

@ -87,6 +87,15 @@ ModalWindow {
currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder));
}
helper.contentsChanged.connect(function() {
if (folderListModel) {
// Make folderListModel refresh.
var save = folderListModel.folder;
folderListModel.folder = "";
folderListModel.folder = save;
}
});
fileTableView.forceActiveFocus();
}
@ -343,12 +352,14 @@ ModalWindow {
onFolderChanged: {
if (folder === rootFolder) {
model = driveListModel;
helper.monitorDirectory("");
update();
} else {
var needsUpdate = model === driveListModel && folder === folderListModel.folder;
model = folderListModel;
folderListModel.folder = folder;
helper.monitorDirectory(helper.urlToPath(folder));
if (needsUpdate) {
update();

View file

@ -24,6 +24,7 @@ Item {
Rectangle { color: hifi.colors.baseGray; anchors.fill: parent; radius: 4 }
Component.onCompleted: {
jointChooser.model = MyAvatar.jointNames;
completed = true;
}
@ -82,7 +83,6 @@ Item {
HifiControls.ComboBox {
id: jointChooser;
anchors { bottom: parent.bottom; left: parent.left; right: parent.right }
model: MyAvatar.jointNames
colorScheme: hifi.colorSchemes.dark
currentIndex: attachment ? model.indexOf(attachment.jointName) : -1
onCurrentIndexChanged: {

View file

@ -78,7 +78,10 @@ Decoration {
id: closeClickArea
anchors.fill: parent
hoverEnabled: true
onClicked: window.shown = false;
onClicked: {
window.shown = false;
window.windowClosed();
}
}
}
}

View file

@ -30,6 +30,7 @@ Fadable {
//
// Signals
//
signal windowClosed();
signal windowDestroyed();
signal mouseEntered();
signal mouseExited();

View file

@ -139,7 +139,6 @@
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
#include "SpeechRecognizer.h"
#endif
#include "Stars.h"
#include "ui/AddressBarDialog.h"
#include "ui/AvatarInputs.h"
#include "ui/DialogsManager.h"
@ -391,6 +390,11 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt
}
static const QString STATE_IN_HMD = "InHMD";
static const QString STATE_CAMERA_FULL_SCREEN_MIRROR = "CameraFSM";
static const QString STATE_CAMERA_FIRST_PERSON = "CameraFirstPerson";
static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson";
static const QString STATE_CAMERA_ENTITY = "CameraEntity";
static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent";
static const QString STATE_SNAP_TURN = "SnapTurn";
static const QString STATE_GROUNDED = "Grounded";
static const QString STATE_NAV_FOCUSED = "NavigationFocused";
@ -470,7 +474,9 @@ bool setupEssentials(int& argc, char** argv) {
DependencyManager::set<InterfaceActionFactory>();
DependencyManager::set<AudioInjectorManager>();
DependencyManager::set<MessagesClient>();
controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } });
controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR,
STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT,
STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } });
DependencyManager::set<UserInputMapper>();
DependencyManager::set<controller::ScriptingInterface, ControllerScriptingInterface>();
DependencyManager::set<InterfaceParentFinder>();
@ -739,7 +745,14 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(&identityPacketTimer, &QTimer::timeout, getMyAvatar(), &MyAvatar::sendIdentityPacket);
identityPacketTimer.start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS);
ResourceCache::setRequestLimit(MAX_CONCURRENT_RESOURCE_DOWNLOADS);
const char** constArgv = const_cast<const char**>(argv);
QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads");
bool success;
int concurrentDownloads = concurrentDownloadsStr.toInt(&success);
if (!success) {
concurrentDownloads = MAX_CONCURRENT_RESOURCE_DOWNLOADS;
}
ResourceCache::setRequestLimit(concurrentDownloads);
_glWidget = new GLCanvas();
getApplicationCompositor().setRenderingWidget(_glWidget);
@ -811,6 +824,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
}
UserActivityLogger::getInstance().logAction("launch", properties);
_connectionMonitor.init();
// Tell our entity edit sender about our known jurisdictions
_entityEditSender.setServerJurisdictions(&_entityServerJurisdictions);
@ -954,6 +968,21 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
_applicationStateDevice->setInputVariant(STATE_IN_HMD, []() -> float {
return qApp->isHMDMode() ? 1 : 0;
});
_applicationStateDevice->setInputVariant(STATE_CAMERA_FULL_SCREEN_MIRROR, []() -> float {
return qApp->getCamera()->getMode() == CAMERA_MODE_MIRROR ? 1 : 0;
});
_applicationStateDevice->setInputVariant(STATE_CAMERA_FIRST_PERSON, []() -> float {
return qApp->getCamera()->getMode() == CAMERA_MODE_FIRST_PERSON ? 1 : 0;
});
_applicationStateDevice->setInputVariant(STATE_CAMERA_THIRD_PERSON, []() -> float {
return qApp->getCamera()->getMode() == CAMERA_MODE_THIRD_PERSON ? 1 : 0;
});
_applicationStateDevice->setInputVariant(STATE_CAMERA_ENTITY, []() -> float {
return qApp->getCamera()->getMode() == CAMERA_MODE_ENTITY ? 1 : 0;
});
_applicationStateDevice->setInputVariant(STATE_CAMERA_INDEPENDENT, []() -> float {
return qApp->getCamera()->getMode() == CAMERA_MODE_INDEPENDENT ? 1 : 0;
});
_applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float {
return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0;
});
@ -1204,6 +1233,17 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(this, &Application::applicationStateChanged, this, &Application::activeChanged);
qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0);
auto textureCache = DependencyManager::get<TextureCache>();
QString skyboxUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-cubemap.jpg" };
QString skyboxAmbientUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-ambient.jpg" };
_defaultSkyboxTexture = textureCache->getImageTexture(skyboxUrl, NetworkTexture::CUBE_TEXTURE, { { "generateIrradiance", false } });
_defaultSkyboxAmbientTexture = textureCache->getImageTexture(skyboxAmbientUrl, NetworkTexture::CUBE_TEXTURE, { { "generateIrradiance", true } });
_defaultSkybox->setCubemap(_defaultSkyboxTexture);
_defaultSkybox->setColor({ 1.0, 1.0, 1.0 });
// After all of the constructor is completed, then set firstRun to false.
Setting::Handle<bool> firstRun{ Settings::firstRun, true };
firstRun.set(false);
@ -2272,7 +2312,7 @@ void Application::keyPressEvent(QKeyEvent* event) {
}
case Qt::Key_Asterisk:
Menu::getInstance()->triggerOption(MenuOption::Stars);
Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox);
break;
case Qt::Key_S:
@ -3240,6 +3280,18 @@ void Application::init() {
getEntities()->setViewFrustum(_viewFrustum);
}
getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) {
auto dims = item.getDimensions();
auto maxSize = glm::max(dims.x, dims.y, dims.z);
if (maxSize <= 0.0f) {
return 0.0f;
}
auto distance = glm::distance(getMyAvatar()->getPosition(), item.getPosition());
return atan2(maxSize, distance);
});
ObjectMotionState::setShapeManager(&_shapeManager);
_physicsEngine->init();
@ -4185,8 +4237,6 @@ public:
typedef render::Payload<BackgroundRenderData> Payload;
typedef Payload::DataPointer Pointer;
Stars _stars;
static render::ItemID _item; // unique WorldBoxRenderData
};
@ -4221,15 +4271,26 @@ namespace render {
// Fall through: if no skybox is available, render the SKY_DOME
case model::SunSkyStage::SKY_DOME: {
if (Menu::getInstance()->isOptionChecked(MenuOption::Stars)) {
PerformanceTimer perfTimer("stars");
PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings),
"Application::payloadRender<BackgroundRenderData>() ... My god, it's full of stars...");
// should be the first rendering pass - w/o depth buffer / lighting
if (Menu::getInstance()->isOptionChecked(MenuOption::DefaultSkybox)) {
static const glm::vec3 DEFAULT_SKYBOX_COLOR { 255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f };
static const float DEFAULT_SKYBOX_INTENSITY { 0.2f };
static const float DEFAULT_SKYBOX_AMBIENT_INTENSITY { 2.0f };
static const glm::vec3 DEFAULT_SKYBOX_DIRECTION { 0.0f, 0.0f, -1.0f };
static const float alpha = 1.0f;
background->_stars.render(args, alpha);
}
auto scene = DependencyManager::get<SceneScriptingInterface>()->getStage();
auto sceneKeyLight = scene->getKeyLight();
scene->setSunModelEnable(false);
sceneKeyLight->setColor(DEFAULT_SKYBOX_COLOR);
sceneKeyLight->setIntensity(DEFAULT_SKYBOX_INTENSITY);
sceneKeyLight->setAmbientIntensity(DEFAULT_SKYBOX_AMBIENT_INTENSITY);
sceneKeyLight->setDirection(DEFAULT_SKYBOX_DIRECTION);
auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture();
sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance());
sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture);
qApp->getDefaultSkybox()->render(batch, args->getViewFrustum());
}
}
break;
@ -4423,7 +4484,6 @@ void Application::updateWindowTitle() const {
#endif
_window->setWindowTitle(title);
}
void Application::clearDomainOctreeDetails() {
// if we're about to quit, we really don't need to do any of these things...
@ -4449,6 +4509,7 @@ void Application::clearDomainOctreeDetails() {
getEntities()->clear();
auto skyStage = DependencyManager::get<SceneScriptingInterface>()->getSkyStage();
skyStage->setBackgroundMode(model::SunSkyStage::SKY_DOME);
_recentlyClearedDomain = true;

View file

@ -47,6 +47,7 @@
#include "avatar/MyAvatar.h"
#include "Bookmarks.h"
#include "Camera.h"
#include "ConnectionMonitor.h"
#include "FileLogger.h"
#include "gpu/Context.h"
#include "Menu.h"
@ -64,6 +65,9 @@
#include "ui/overlays/Overlays.h"
#include "UndoStackScriptingInterface.h"
#include <procedural/ProceduralSkybox.h>
#include <model/Skybox.h>
class OffscreenGLCanvas;
class GLCanvas;
class FaceTracker;
@ -251,6 +255,10 @@ public:
void takeSnapshot(bool notify);
void shareSnapshot(const QString& filename);
model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; }
gpu::TexturePointer getDefaultSkyboxTexture() const { return _defaultSkyboxTexture; }
gpu::TexturePointer getDefaultSkyboxAmbientTexture() const { return _defaultSkyboxAmbientTexture; }
signals:
void svoImportRequested(const QString& url);
@ -563,6 +571,13 @@ private:
bool _recentlyClearedDomain { false };
QString _returnFromFullScreenMirrorTo;
ConnectionMonitor _connectionMonitor;
model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() } ;
gpu::TexturePointer _defaultSkyboxTexture;
gpu::TexturePointer _defaultSkyboxAmbientTexture;
};
#endif // hifi_Application_h

View file

@ -0,0 +1,52 @@
//
// ConnectionMonitor.cpp
// interface/src
//
// Created by Ryan Huffman on 8/4/15.
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "ConnectionMonitor.h"
#include "ui/DialogsManager.h"
#include <NodeList.h>
#include <DependencyManager.h>
#include <DomainHandler.h>
#include <AddressManager.h>
static const int DISPLAY_AFTER_DISCONNECTED_FOR_X_MS = 5000;
void ConnectionMonitor::init() {
// Connect to domain disconnected message
auto nodeList = DependencyManager::get<NodeList>();
const DomainHandler& domainHandler = nodeList->getDomainHandler();
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &ConnectionMonitor::disconnectedFromDomain);
connect(&domainHandler, &DomainHandler::connectedToDomain, this, &ConnectionMonitor::connectedToDomain);
// Connect to AddressManager::hostChanged
auto addressManager = DependencyManager::get<AddressManager>();
connect(addressManager.data(), &AddressManager::hostChanged, this, &ConnectionMonitor::hostChanged);
_timer.setSingleShot(true);
_timer.setInterval(DISPLAY_AFTER_DISCONNECTED_FOR_X_MS);
_timer.start();
auto dialogsManager = DependencyManager::get<DialogsManager>();
connect(&_timer, &QTimer::timeout, dialogsManager.data(), &DialogsManager::showAddressBar);
}
void ConnectionMonitor::disconnectedFromDomain() {
_timer.start();
}
void ConnectionMonitor::connectedToDomain(const QString& name) {
_timer.stop();
}
void ConnectionMonitor::hostChanged(const QString& name) {
_timer.start();
}

View file

@ -0,0 +1,34 @@
//
// ConnectionMonitor.h
// interface/src
//
// Created by Ryan Huffman on 8/4/15.
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_ConnectionMonitor_h
#define hifi_ConnectionMonitor_h
#include <QObject>
#include <QTimer>
class QString;
class ConnectionMonitor : public QObject {
Q_OBJECT
public:
void init();
private slots:
void disconnectedFromDomain();
void connectedToDomain(const QString& name);
void hostChanged(const QString& name);
private:
QTimer _timer;
};
#endif // hifi_ConnectionMonitor_h

View file

@ -337,7 +337,7 @@ Menu::Menu() {
// Developer > Render >>>
MenuWrapper* renderOptionsMenu = developerMenu->addMenu("Render");
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::WorldAxes);
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Stars, 0, true);
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::DefaultSkybox, 0, true);
// Developer > Render > Throttle FPS If Not Focus
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ThrottleFPSIfNotFocus, 0, true);

View file

@ -80,6 +80,7 @@ namespace MenuOption {
const QString CrashNewFaultThreaded = "New Fault (threaded)";
const QString DeadlockInterface = "Deadlock Interface";
const QString DecreaseAvatarSize = "Decrease Avatar Size";
const QString DefaultSkybox = "Default Skybox";
const QString DeleteBookmark = "Delete Bookmark...";
const QString DisableActivityLogger = "Disable Activity Logger";
const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment";
@ -175,7 +176,6 @@ namespace MenuOption {
const QString StandingHMDSensorMode = "Standing HMD Sensor Mode";
const QString SimulateEyeTracking = "Simulate";
const QString SMIEyeTracking = "SMI Eye Tracking";
const QString Stars = "Stars";
const QString Stats = "Stats";
const QString StopAllScripts = "Stop All Scripts";
const QString SuppressShortTimings = "Suppress Timings Less than 10ms";

View file

@ -1,216 +0,0 @@
//
// Stars.cpp
// interface/src
//
// Created by Tobias Schwinger on 3/22/13.
// Copyright 2013 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 "Stars.h"
#include <mutex>
#include <QElapsedTimer>
#include <NumericalConstants.h>
#include <DependencyManager.h>
#include <GeometryCache.h>
#include <TextureCache.h>
#include <RenderArgs.h>
#include <ViewFrustum.h>
#include <render-utils/stars_vert.h>
#include <render-utils/stars_frag.h>
#include <render-utils/standardTransformPNTC_vert.h>
#include <render-utils/starsGrid_frag.h>
//static const float TILT = 0.23f;
static const float TILT = 0.0f;
static const unsigned int STARFIELD_NUM_STARS = 50000;
static const unsigned int STARFIELD_SEED = 1;
static const float STAR_COLORIZATION = 0.1f;
static const float TAU = 6.28318530717958f;
//static const float HALF_TAU = TAU / 2.0f;
//static const float QUARTER_TAU = TAU / 4.0f;
//static const float MILKY_WAY_WIDTH = TAU / 30.0f; // width in radians of one half of the Milky Way
//static const float MILKY_WAY_INCLINATION = 0.0f; // angle of Milky Way from horizontal in degrees
//static const float MILKY_WAY_RATIO = 0.4f;
static const char* UNIFORM_TIME_NAME = "iGlobalTime";
// Produce a random float value between 0 and 1
static float frand() {
return (float)rand() / (float)RAND_MAX;
}
// http://mathworld.wolfram.com/SpherePointPicking.html
static vec2 randPolar() {
vec2 result(frand(), frand());
result.x *= TAU;
result.y = powf(result.y, 2.0) / 2.0f;
if (frand() > 0.5f) {
result.y = 0.5f - result.y;
} else {
result.y += 0.5f;
}
result.y = acos((2.0f * result.y) - 1.0f);
return result;
}
static vec3 fromPolar(const vec2& polar) {
float sinTheta = sin(polar.x);
float cosTheta = cos(polar.x);
float sinPhi = sin(polar.y);
float cosPhi = cos(polar.y);
return vec3(
cosTheta * sinPhi,
cosPhi,
sinTheta * sinPhi);
}
// computeStarColor
// - Generate a star color.
//
// colorization can be a value between 0 and 1 specifying how colorful the resulting star color is.
//
// 0 = completely black & white
// 1 = very colorful
unsigned computeStarColor(float colorization) {
unsigned char red, green, blue;
if (randFloat() < 0.3f) {
// A few stars are colorful
red = 2 + (rand() % 254);
green = 2 + round((red * (1 - colorization)) + ((rand() % 254) * colorization));
blue = 2 + round((red * (1 - colorization)) + ((rand() % 254) * colorization));
} else {
// Most stars are dimmer and white
red = green = blue = 2 + (rand() % 128);
}
return red | (green << 8) | (blue << 16);
}
struct StarVertex {
vec4 position;
vec4 colorAndSize;
};
static const int STARS_VERTICES_SLOT{ 0 };
static const int STARS_COLOR_SLOT{ 1 };
gpu::PipelinePointer Stars::_gridPipeline{};
gpu::PipelinePointer Stars::_starsPipeline{};
int32_t Stars::_timeSlot{ -1 };
void Stars::init() {
if (!_gridPipeline) {
auto vs = gpu::Shader::createVertex(std::string(standardTransformPNTC_vert));
auto ps = gpu::Shader::createPixel(std::string(starsGrid_frag));
auto program = gpu::Shader::createProgram(vs, ps);
gpu::Shader::makeProgram((*program));
_timeSlot = program->getBuffers().findLocation(UNIFORM_TIME_NAME);
if (_timeSlot == gpu::Shader::INVALID_LOCATION) {
_timeSlot = program->getUniforms().findLocation(UNIFORM_TIME_NAME);
}
auto state = gpu::StatePointer(new gpu::State());
// enable decal blend
state->setDepthTest(gpu::State::DepthTest(false));
state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP));
state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA);
_gridPipeline = gpu::Pipeline::create(program, state);
}
if (!_starsPipeline) {
auto vs = gpu::Shader::createVertex(std::string(stars_vert));
auto ps = gpu::Shader::createPixel(std::string(stars_frag));
auto program = gpu::Shader::createProgram(vs, ps);
gpu::Shader::makeProgram((*program));
auto state = gpu::StatePointer(new gpu::State());
// enable decal blend
state->setDepthTest(gpu::State::DepthTest(false));
state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP));
state->setAntialiasedLineEnable(true); // line smoothing also smooth points
state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA);
_starsPipeline = gpu::Pipeline::create(program, state);
}
unsigned limit = STARFIELD_NUM_STARS;
std::vector<StarVertex> points;
points.resize(limit);
{ // generate stars
QElapsedTimer startTime;
startTime.start();
vertexBuffer.reset(new gpu::Buffer);
srand(STARFIELD_SEED);
for (size_t star = 0; star < limit; ++star) {
points[star].position = vec4(fromPolar(randPolar()), 1);
float size = frand() * 2.5f + 0.5f;
if (frand() < STAR_COLORIZATION) {
vec3 color(frand() / 2.0f + 0.5f, frand() / 2.0f + 0.5f, frand() / 2.0f + 0.5f);
points[star].colorAndSize = vec4(color, size);
} else {
vec3 color(frand() / 2.0f + 0.5f);
points[star].colorAndSize = vec4(color, size);
}
}
double timeDiff = (double)startTime.nsecsElapsed() / 1000000.0; // ns to ms
qDebug() << "Total time to generate stars: " << timeDiff << " msec";
}
gpu::Element positionElement, colorElement;
const size_t VERTEX_STRIDE = sizeof(StarVertex);
vertexBuffer->append(VERTEX_STRIDE * limit, (const gpu::Byte*)&points[0]);
streamFormat.reset(new gpu::Stream::Format()); // 1 for everyone
streamFormat->setAttribute(gpu::Stream::POSITION, STARS_VERTICES_SLOT, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW), 0);
streamFormat->setAttribute(gpu::Stream::COLOR, STARS_COLOR_SLOT, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::RGBA));
positionElement = streamFormat->getAttributes().at(gpu::Stream::POSITION)._element;
colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element;
size_t offset = offsetof(StarVertex, position);
positionView = gpu::BufferView(vertexBuffer, offset, vertexBuffer->getSize(), VERTEX_STRIDE, positionElement);
offset = offsetof(StarVertex, colorAndSize);
colorView = gpu::BufferView(vertexBuffer, offset, vertexBuffer->getSize(), VERTEX_STRIDE, colorElement);
}
// FIXME star colors
void Stars::render(RenderArgs* renderArgs, float alpha) {
std::call_once(once, [&]{ init(); });
auto modelCache = DependencyManager::get<ModelCache>();
auto textureCache = DependencyManager::get<TextureCache>();
auto geometryCache = DependencyManager::get<GeometryCache>();
gpu::Batch& batch = *renderArgs->_batch;
batch.setViewTransform(Transform());
batch.setProjectionTransform(renderArgs->getViewFrustum().getProjection());
batch.setModelTransform(Transform().setRotation(glm::inverse(renderArgs->getViewFrustum().getOrientation()) *
quat(vec3(TILT, 0, 0))));
batch.setResourceTexture(0, textureCache->getWhiteTexture());
// Render the world lines
batch.setPipeline(_gridPipeline);
static auto start = usecTimestampNow();
float msecs = (float)(usecTimestampNow() - start) / (float)USECS_PER_MSEC;
float secs = msecs / (float)MSECS_PER_SECOND;
batch._glUniform1f(_timeSlot, secs);
geometryCache->renderCube(batch);
// Render the stars
batch.setPipeline(_starsPipeline);
batch.setInputFormat(streamFormat);
batch.setInputBuffer(STARS_VERTICES_SLOT, positionView);
batch.setInputBuffer(STARS_COLOR_SLOT, colorView);
batch.draw(gpu::Primitive::POINTS, STARFIELD_NUM_STARS);
}

View file

@ -1,49 +0,0 @@
//
// Stars.h
// interface/src
//
// Created by Tobias Schwinger on 3/22/13.
// Copyright 2013 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_Stars_h
#define hifi_Stars_h
#include <gpu/Context.h>
class RenderArgs;
// Starfield rendering component.
class Stars {
public:
Stars() = default;
~Stars() = default;
Stars(Stars const&) = delete;
Stars& operator=(Stars const&) = delete;
// Renders the starfield from a local viewer's perspective.
// The parameters specifiy the field of view.
void render(RenderArgs* args, float alpha);
private:
// Pipelines
static gpu::PipelinePointer _gridPipeline;
static gpu::PipelinePointer _starsPipeline;
static int32_t _timeSlot;
// Buffers
gpu::BufferPointer vertexBuffer;
gpu::Stream::FormatPointer streamFormat;
gpu::BufferView positionView;
gpu::BufferView colorView;
std::once_flag once;
void init();
};
#endif // hifi_Stars_h

View file

@ -98,6 +98,7 @@ Avatar::Avatar(RigPointer rig) :
_headData = static_cast<HeadData*>(new Head(this));
_skeletonModel = std::make_shared<SkeletonModel>(this, nullptr, rig);
connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished);
}
Avatar::~Avatar() {
@ -298,7 +299,9 @@ void Avatar::simulate(float deltaTime) {
{
PerformanceTimer perfTimer("head");
glm::vec3 headPosition = getPosition();
_skeletonModel->getHeadPosition(headPosition);
if (!_skeletonModel->getHeadPosition(headPosition)) {
headPosition = getPosition();
}
Head* head = getHead();
head->setPosition(headPosition);
head->setScale(getUniformScale());
@ -306,6 +309,7 @@ void Avatar::simulate(float deltaTime) {
}
} else {
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
getHead()->setPosition(getPosition());
_skeletonModel->simulate(deltaTime, false);
}
@ -776,7 +780,7 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const
{
PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderBevelCornersRect");
DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, true, true, true);
DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, true, true, true);
DependencyManager::get<GeometryCache>()->renderBevelCornersRect(batch, left, bottom, width, height,
bevelDistance, backgroundColor);
}
@ -916,6 +920,17 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
}
}
void Avatar::setModelURLFinished(bool success) {
if (!success && _skeletonModelURL != AvatarData::defaultFullAvatarModelUrl()) {
qDebug() << "Using default after failing to load Avatar model: " << _skeletonModelURL;
// call _skeletonModel.setURL, but leave our copy of _skeletonModelURL alone. This is so that
// we don't redo this every time we receive an identity packet from the avatar with the bad url.
QMetaObject::invokeMethod(_skeletonModel.get(), "setURL",
Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl()));
}
}
// create new model, can return an instance of a SoftAttachmentModel rather then Model
static std::shared_ptr<Model> allocateAttachmentModel(bool isSoft, RigPointer rigOverride) {
if (isSoft) {

View file

@ -184,6 +184,8 @@ public slots:
glm::vec3 getRightPalmPosition() const;
glm::quat getRightPalmRotation() const;
void setModelURLFinished(bool success);
protected:
friend class AvatarManager;

View file

@ -430,6 +430,7 @@ void MyAvatar::simulate(float deltaTime) {
if (!_skeletonModel->hasSkeleton()) {
// All the simulation that can be done has been done
getHead()->setPosition(getPosition()); // so audio-position isn't 0,0,0
return;
}

View file

@ -62,8 +62,10 @@ void AssetMappingsScriptingInterface::getMapping(QString path, QJSValue callback
auto request = assetClient->createGetMappingRequest(path);
connect(request, &GetMappingRequest::finished, this, [this, callback](GetMappingRequest* request) mutable {
auto hash = request->getHash();
if (callback.isCallable()) {
QJSValueList args { request->getErrorString() };
QJSValueList args { request->getErrorString(), hash };
callback.call(args);
}

View file

@ -28,6 +28,8 @@
#include "Util.h"
#include "ui/Stats.h"
#include "ui/AvatarInputs.h"
#include "OffscreenUi.h"
#include <QQmlContext>
const vec4 CONNECTION_STATUS_BORDER_COLOR{ 1.0f, 0.0f, 0.0f, 0.8f };
static const float ORTHO_NEAR_CLIP = -1000.0f;
@ -177,13 +179,11 @@ void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) {
glm::vec2 texCoordMinCorner(0.0f, 0.0f);
glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight()));
geometryCache->useSimpleDrawPipeline(batch, true);
batch.setResourceTexture(0, selfieTexture);
geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f));
float alpha = DependencyManager::get<OffscreenUi>()->getDesktop()->property("unpinnedAlpha").toFloat();
geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha));
batch.setResourceTexture(0, renderArgs->_whiteTexture);
geometryCache->useSimpleDrawPipeline(batch, false);
}
}

View file

@ -50,6 +50,10 @@ void DialogsManager::toggleAddressBar() {
emit addressBarToggled();
}
void DialogsManager::showAddressBar() {
AddressBarDialog::show();
}
void DialogsManager::toggleDiskCacheEditor() {
maybeCreateDialog(_diskCacheEditor);
_diskCacheEditor->toggle();

View file

@ -44,6 +44,7 @@ public:
public slots:
void toggleAddressBar();
void showAddressBar();
void toggleDiskCacheEditor();
void toggleLoginDialog();
void showLoginDialog();

View file

@ -47,7 +47,7 @@ void Cube3DOverlay::render(RenderArgs* args) {
auto geometryCache = DependencyManager::get<GeometryCache>();
auto pipeline = args->_pipeline;
if (!pipeline) {
pipeline = _isSolid ? geometryCache->getShapePipeline() : geometryCache->getWireShapePipeline();
pipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline();
}
if (_isSolid) {
@ -55,7 +55,7 @@ void Cube3DOverlay::render(RenderArgs* args) {
batch->setModelTransform(transform);
geometryCache->renderSolidCubeInstance(*batch, cubeColor, pipeline);
} else {
geometryCache->bindSimpleProgram(*batch, false, false, true, true);
geometryCache->bindSimpleProgram(*batch, false, false, false, true, true);
if (getIsDashedLine()) {
transform.setScale(1.0f);
batch->setModelTransform(transform);

View file

@ -57,12 +57,12 @@ void Line3DOverlay::render(RenderArgs* args) {
auto geometryCache = DependencyManager::get<GeometryCache>();
if (getIsDashedLine()) {
// TODO: add support for color to renderDashedLine()
geometryCache->bindSimpleProgram(*batch, false, false, true, true);
geometryCache->bindSimpleProgram(*batch, false, false, false, true, true);
geometryCache->renderDashedLine(*batch, _start, _end, colorv4, _geometryCacheID);
} else if (_glow > 0.0f) {
geometryCache->renderGlowLine(*batch, _start, _end, colorv4, _glow, _glowWidth, _geometryCacheID);
} else {
geometryCache->bindSimpleProgram(*batch, false, false, true, true);
geometryCache->bindSimpleProgram(*batch, false, false, false, true, true);
geometryCache->renderLine(*batch, _start, _end, colorv4, _geometryCacheID);
}
}

View file

@ -93,7 +93,7 @@ public slots:
/// successful edit, if the input id is for an unknown overlay this function will have no effect
bool editOverlays(const QVariant& propertiesById);
/// deletes a particle
/// deletes an overlay
void deleteOverlay(unsigned int id);
/// get the string type of the overlay used in addOverlay

View file

@ -61,7 +61,7 @@ void Rectangle3DOverlay::render(RenderArgs* args) {
geometryCache->bindSimpleProgram(*batch);
geometryCache->renderQuad(*batch, topLeft, bottomRight, rectangleColor);
} else {
geometryCache->bindSimpleProgram(*batch, false, false, true, true);
geometryCache->bindSimpleProgram(*batch, false, false, false, true, true);
if (getIsDashedLine()) {
glm::vec3 point1(-halfDimensions.x, -halfDimensions.y, 0.0f);
glm::vec3 point2(halfDimensions.x, -halfDimensions.y, 0.0f);

View file

@ -47,7 +47,7 @@ void Shape3DOverlay::render(RenderArgs* args) {
auto geometryCache = DependencyManager::get<GeometryCache>();
auto pipeline = args->_pipeline;
if (!pipeline) {
pipeline = _isSolid ? geometryCache->getShapePipeline() : geometryCache->getWireShapePipeline();
pipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline();
}
transform.setScale(dimensions);

View file

@ -46,7 +46,7 @@ void Sphere3DOverlay::render(RenderArgs* args) {
auto geometryCache = DependencyManager::get<GeometryCache>();
auto pipeline = args->_pipeline;
if (!pipeline) {
pipeline = _isSolid ? geometryCache->getShapePipeline() : geometryCache->getWireShapePipeline();
pipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline();
}
if (_isSolid) {

View file

@ -119,6 +119,9 @@ AudioClient::AudioClient() :
this, &AudioClient::processReceivedSamples, Qt::DirectConnection);
connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { switchOutputToAudioDevice(outputDeviceInfo); });
connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioClient::handleMismatchAudioFormat);
_inputDevices = getDeviceNames(QAudio::AudioInput);
_outputDevices = getDeviceNames(QAudio::AudioOutput);
@ -147,6 +150,12 @@ AudioClient::~AudioClient() {
}
}
void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) {
qDebug() << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec;
selectAudioFormat(recievedCodec);
}
void AudioClient::reset() {
_receivedAudioStream.reset();
_stats.reset();
@ -532,7 +541,13 @@ void AudioClient::negotiateAudioFormat() {
}
void AudioClient::handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message) {
_selectedCodecName = message->readString();
QString selectedCodecName = message->readString();
selectAudioFormat(selectedCodecName);
}
void AudioClient::selectAudioFormat(const QString& selectedCodecName) {
_selectedCodecName = selectedCodecName;
qDebug() << "Selected Codec:" << _selectedCodecName;

View file

@ -104,6 +104,7 @@ public:
};
void negotiateAudioFormat();
void selectAudioFormat(const QString& selectedCodecName);
const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; }
MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; }
@ -153,6 +154,7 @@ public slots:
void handleNoisyMutePacket(QSharedPointer<ReceivedMessage> message);
void handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> message);
void handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message);
void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec);
void sendDownstreamAudioStatsPacket() { _stats.sendDownstreamAudioStatsPacket(); }
void handleAudioInput();

View file

@ -147,7 +147,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) {
writeDroppableSilentSamples(networkSamples);
// inform others of the mismatch
auto sendingNode = DependencyManager::get<NodeList>()->nodeWithUUID(message.getSourceID());
emit mismatchedAudioCodec(sendingNode, _selectedCodecName);
emit mismatchedAudioCodec(sendingNode, _selectedCodecName, codecInPacket);
}
}
break;

View file

@ -182,7 +182,7 @@ public:
void cleanupCodec();
signals:
void mismatchedAudioCodec(SharedNodePointer sendingNode, const QString& desiredCodec);
void mismatchedAudioCodec(SharedNodePointer sendingNode, const QString& currentCodec, const QString& recievedCodec);
public slots:
/// This function should be called every second for all the stats to function properly. If dynamic jitter buffers

View file

@ -1638,7 +1638,9 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) {
for (const auto& attachmentVar : variant) {
AttachmentData attachment;
attachment.fromVariant(attachmentVar);
newAttachments.append(attachment);
if (!attachment.modelURL.isEmpty()) {
newAttachments.append(attachment);
}
}
setAttachmentData(newAttachments);
}

View file

@ -375,7 +375,6 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
}
skyStage->setBackgroundMode(model::SunSkyStage::SKY_DOME); // let the application background through
return; // Early exit
}
@ -529,7 +528,7 @@ void EntityTreeRenderer::processEraseMessage(ReceivedMessage& message, const Sha
std::static_pointer_cast<EntityTree>(_tree)->processEraseMessage(message, sourceNode);
}
ModelPointer EntityTreeRenderer::allocateModel(const QString& url, const QString& collisionUrl) {
ModelPointer EntityTreeRenderer::allocateModel(const QString& url, const QString& collisionUrl, float loadingPriority) {
ModelPointer model = nullptr;
// Only create and delete models on the thread that owns the EntityTreeRenderer
@ -543,6 +542,7 @@ ModelPointer EntityTreeRenderer::allocateModel(const QString& url, const QString
}
model = std::make_shared<Model>(std::make_shared<Rig>());
model->setLoadingPriority(loadingPriority);
model->init();
model->setURL(QUrl(url));
model->setCollisionModelURL(QUrl(collisionUrl));

View file

@ -28,11 +28,14 @@ class AbstractViewStateInterface;
class Model;
class ScriptEngine;
class ZoneEntityItem;
class EntityItem;
class Model;
using ModelPointer = std::shared_ptr<Model>;
using ModelWeakPointer = std::weak_ptr<Model>;
using CalculateEntityLoadingPriority = std::function<float(const EntityItem& item)>;
// Generic client side Octree renderer class.
class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService, public Dependency {
Q_OBJECT
@ -46,6 +49,10 @@ public:
virtual PacketType getExpectedPacketType() const { return PacketType::EntityData; }
virtual void setTree(OctreePointer newTree);
// Returns the priority at which an entity should be loaded. Higher values indicate higher priority.
float getEntityLoadingPriority(const EntityItem& item) const { return _calculateEntityLoadingPriorityFunc(item); }
void setEntityLoadingPriorityFunction(CalculateEntityLoadingPriority fn) { this->_calculateEntityLoadingPriorityFunc = fn; }
void shutdown();
void update();
@ -66,7 +73,7 @@ public:
void reloadEntityScripts();
/// if a renderable entity item needs a model, we will allocate it for them
Q_INVOKABLE ModelPointer allocateModel(const QString& url, const QString& collisionUrl);
Q_INVOKABLE ModelPointer allocateModel(const QString& url, const QString& collisionUrl, float loadingPriority = 0.0f);
/// if a renderable entity item needs to update the URL of a model, we will handle that for the entity
Q_INVOKABLE ModelPointer updateModel(ModelPointer original, const QString& newUrl, const QString& collisionUrl);
@ -202,6 +209,10 @@ private:
QList<EntityItemID> _entityIDsLastInScene;
static int _entitiesScriptEngineCount;
CalculateEntityLoadingPriority _calculateEntityLoadingPriorityFunc = [](const EntityItem& item) -> float {
return 0.0f;
};
};

View file

@ -19,7 +19,7 @@ namespace render {
if (payload->entity->getType() == EntityTypes::Light) {
return ItemKey::Builder::light();
}
if (payload && payload->entity->getType() == EntityTypes::PolyLine) {
if (payload && payload->entity->isTransparent()) {
return ItemKey::Builder::transparentShape();
}
}

View file

@ -96,8 +96,17 @@ public: \
virtual void removeFromScene(EntityItemPointer self, std::shared_ptr<render::Scene> scene, render::PendingChanges& pendingChanges) override { _renderHelper.removeFromScene(self, scene, pendingChanges); } \
virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); _renderHelper.notifyChanged(); } \
virtual void dimensionsChanged() override { EntityItem::dimensionsChanged(); _renderHelper.notifyChanged(); } \
void checkFading() { \
bool transparent = isTransparent(); \
if (transparent != _prevIsTransparent) { \
_renderHelper.notifyChanged(); \
_isFading = false; \
_prevIsTransparent = transparent; \
} \
} \
private: \
SimpleRenderableEntityItem _renderHelper;
SimpleRenderableEntityItem _renderHelper; \
bool _prevIsTransparent { isTransparent() };
#endif // hifi_RenderableEntityItem_h

View file

@ -28,6 +28,8 @@ EntityItemPointer RenderableLightEntityItem::factory(const EntityItemID& entityI
void RenderableLightEntityItem::render(RenderArgs* args) {
PerformanceTimer perfTimer("RenderableLightEntityItem::render");
assert(getType() == EntityTypes::Light);
checkFading();
glm::vec3 position = getPosition();
glm::vec3 dimensions = getDimensions();
glm::quat rotation = getRotation();
@ -35,7 +37,7 @@ void RenderableLightEntityItem::render(RenderArgs* args) {
glm::vec3 color = toGlm(getXColor());
float intensity = getIntensity();
float intensity = getIntensity() * (_isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f);
float falloffRadius = getFalloffRadius();
float exponent = getExponent();
float cutoff = glm::radians(getCutoff());

View file

@ -176,25 +176,6 @@ void RenderableModelEntityItem::doInitialModelSimulation() {
_needsInitialSimulation = false;
}
// TODO: we need a solution for changes to the postion/rotation/etc of a model...
// this current code path only addresses that in this setup case... not the changing/moving case
bool RenderableModelEntityItem::readyToAddToScene(RenderArgs* renderArgs) {
if (!_model && renderArgs) {
// TODO: this getModel() appears to be about 3% of model render time. We should optimize
PerformanceTimer perfTimer("getModel");
EntityTreeRenderer* renderer = static_cast<EntityTreeRenderer*>(renderArgs->_renderer);
getModel(renderer);
}
if (renderArgs && _model && _needsInitialSimulation && _model->isActive() && _model->isLoaded()) {
// make sure to simulate so everything gets set up correctly for rendering
doInitialModelSimulation();
_model->renderSetup(renderArgs);
}
bool ready = !_needsInitialSimulation && _model && _model->readyToAddToScene(renderArgs);
return ready;
}
class RenderableModelEntityItemMeta {
public:
RenderableModelEntityItemMeta(EntityItemPointer entity) : entity(entity){ }
@ -371,6 +352,12 @@ void RenderableModelEntityItem::render(RenderArgs* args) {
PerformanceTimer perfTimer("RMEIrender");
assert(getType() == EntityTypes::Model);
// When the individual mesh parts of a model finish fading, they will mark their Model as needing updating
// we will watch for that and ask the model to update it's render items
if (_model && _model->getRenderItemsNeedUpdate()) {
_model->updateRenderItems();
}
if (hasModel()) {
// Prepare the current frame
{
@ -484,7 +471,7 @@ ModelPointer RenderableModelEntityItem::getModel(EntityTreeRenderer* renderer) {
if (!getModelURL().isEmpty()) {
// If we don't have a model, allocate one *immediately*
if (!_model) {
_model = _myRenderer->allocateModel(getModelURL(), getCompoundShapeURL());
_model = _myRenderer->allocateModel(getModelURL(), getCompoundShapeURL(), renderer->getEntityLoadingPriority(*this));
_needsInitialSimulation = true;
// If we need to change URLs, update it *after rendering* (to avoid access violations)
} else if ((QUrl(getModelURL()) != _model->getURL() || QUrl(getCompoundShapeURL()) != _model->getCollisionURL())) {
@ -916,6 +903,18 @@ bool RenderableModelEntityItem::contains(const glm::vec3& point) const {
return false;
}
bool RenderableModelEntityItem::shouldBePhysical() const {
// If we have a model, make sure it hasn't failed to download.
// If it has, we'll report back that we shouldn't be physical so that physics aren't held waiting for us to be ready.
if (_model && getShapeType() == SHAPE_TYPE_COMPOUND && _model->didCollisionGeometryRequestFail()) {
return false;
} else if (_model && getShapeType() != SHAPE_TYPE_NONE && _model->didVisualGeometryRequestFail()) {
return false;
} else {
return ModelEntityItem::shouldBePhysical();
}
}
glm::quat RenderableModelEntityItem::getAbsoluteJointRotationInObjectFrame(int index) const {
if (_model) {
glm::quat result;

View file

@ -40,7 +40,6 @@ public:
void doInitialModelSimulation();
virtual bool readyToAddToScene(RenderArgs* renderArgs = nullptr);
virtual bool addToScene(EntityItemPointer self, std::shared_ptr<render::Scene> scene, render::PendingChanges& pendingChanges) override;
virtual void removeFromScene(EntityItemPointer self, std::shared_ptr<render::Scene> scene, render::PendingChanges& pendingChanges) override;
@ -52,7 +51,6 @@ public:
bool& keepSearching, OctreeElementPointer& element, float& distance,
BoxFace& face, glm::vec3& surfaceNormal,
void** intersectedObject, bool precisionPicking) const override;
ModelPointer getModel(EntityTreeRenderer* renderer);
virtual bool needsToCallUpdate() const override;
@ -65,6 +63,8 @@ public:
virtual bool contains(const glm::vec3& point) const override;
virtual bool shouldBePhysical() const override;
// these are in the frame of this object (model space)
virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override;
virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override;
@ -91,6 +91,9 @@ public:
render::ItemID getMetaRenderItem() { return _myMetaItem; }
// Transparency is handled in ModelMeshPartPayload
bool isTransparent() override { return false; }
private:
QVariantMap parseTexturesToMap(QString textures);
void remapTextures();

View file

@ -167,6 +167,8 @@ void RenderablePolyLineEntityItem::update(const quint64& now) {
}
void RenderablePolyLineEntityItem::render(RenderArgs* args) {
checkFading();
QWriteLocker lock(&_quadReadWriteLock);
if (_points.size() < 2 || _normals.size () < 2 || _strokeWidths.size() < 2) {
return;
@ -204,5 +206,9 @@ void RenderablePolyLineEntityItem::render(RenderArgs* args) {
batch.setInputFormat(_format);
batch.setInputBuffer(0, _verticesBuffer, 0, _format->getChannels().at(0)._stride);
if (_isFading) {
batch._glColor4f(1.0f, 1.0f, 1.0f, Interpolate::calculateFadeRatio(_fadeStartTime));
}
batch.draw(gpu::TRIANGLE_STRIP, _numVertices, 0);
};

View file

@ -30,7 +30,9 @@ public:
virtual void render(RenderArgs* args) override;
virtual void update(const quint64& now) override;
virtual bool needsToCallUpdate() const override { return true; };
virtual bool needsToCallUpdate() const override { return true; }
bool isTransparent() override { return true; }
SIMPLE_RENDERABLE();
@ -47,7 +49,6 @@ protected:
gpu::BufferView _uniformBuffer;
unsigned int _numVertices;
QVector<glm::vec3> _vertices;
};

View file

@ -50,7 +50,7 @@ public:
void initializePolyVox();
virtual void somethingChangedNotification() {
virtual void somethingChangedNotification() override {
// This gets called from EnityItem::readEntityDataFromBuffer every time a packet describing
// this entity comes from the entity-server. It gets called even if nothing has actually changed
// (see the comment in EntityItem.cpp). If that gets fixed, this could be used to know if we
@ -58,19 +58,19 @@ public:
// _needsModelReload = true;
}
virtual uint8_t getVoxel(int x, int y, int z);
virtual bool setVoxel(int x, int y, int z, uint8_t toValue);
virtual uint8_t getVoxel(int x, int y, int z) override;
virtual bool setVoxel(int x, int y, int z, uint8_t toValue) override;
void render(RenderArgs* args);
virtual bool supportsDetailedRayIntersection() const { return true; }
void render(RenderArgs* args) override;
virtual bool supportsDetailedRayIntersection() const override { return true; }
virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
bool& keepSearching, OctreeElementPointer& element, float& distance,
BoxFace& face, glm::vec3& surfaceNormal,
void** intersectedObject, bool precisionPicking) const;
void** intersectedObject, bool precisionPicking) const override;
virtual void setVoxelData(QByteArray voxelData);
virtual void setVoxelVolumeSize(glm::vec3 voxelVolumeSize);
virtual void setVoxelSurfaceStyle(PolyVoxSurfaceStyle voxelSurfaceStyle);
virtual void setVoxelData(QByteArray voxelData) override;
virtual void setVoxelVolumeSize(glm::vec3 voxelVolumeSize) override;
virtual void setVoxelSurfaceStyle(PolyVoxSurfaceStyle voxelSurfaceStyle) override;
glm::vec3 getSurfacePositionAdjustment() const;
glm::mat4 voxelToWorldMatrix() const;
@ -78,45 +78,45 @@ public:
glm::mat4 voxelToLocalMatrix() const;
glm::mat4 localToVoxelMatrix() const;
virtual ShapeType getShapeType() const;
virtual bool shouldBePhysical() const { return !isDead(); }
virtual bool isReadyToComputeShape();
virtual void computeShapeInfo(ShapeInfo& info);
virtual ShapeType getShapeType() const override;
virtual bool shouldBePhysical() const override { return !isDead(); }
virtual bool isReadyToComputeShape() override;
virtual void computeShapeInfo(ShapeInfo& info) override;
virtual glm::vec3 voxelCoordsToWorldCoords(glm::vec3& voxelCoords) const;
virtual glm::vec3 worldCoordsToVoxelCoords(glm::vec3& worldCoords) const;
virtual glm::vec3 voxelCoordsToLocalCoords(glm::vec3& voxelCoords) const;
virtual glm::vec3 localCoordsToVoxelCoords(glm::vec3& localCoords) const;
virtual glm::vec3 voxelCoordsToWorldCoords(glm::vec3& voxelCoords) const override;
virtual glm::vec3 worldCoordsToVoxelCoords(glm::vec3& worldCoords) const override;
virtual glm::vec3 voxelCoordsToLocalCoords(glm::vec3& voxelCoords) const override;
virtual glm::vec3 localCoordsToVoxelCoords(glm::vec3& localCoords) const override;
// coords are in voxel-volume space
virtual bool setSphereInVolume(glm::vec3 center, float radius, uint8_t toValue);
virtual bool setVoxelInVolume(glm::vec3 position, uint8_t toValue);
virtual bool setSphereInVolume(glm::vec3 center, float radius, uint8_t toValue) override;
virtual bool setVoxelInVolume(glm::vec3 position, uint8_t toValue) override;
// coords are in world-space
virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue);
virtual bool setAll(uint8_t toValue);
virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue);
virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue) override;
virtual bool setAll(uint8_t toValue) override;
virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue) override;
virtual void setXTextureURL(QString xTextureURL);
virtual void setYTextureURL(QString yTextureURL);
virtual void setZTextureURL(QString zTextureURL);
virtual void setXTextureURL(QString xTextureURL) override;
virtual void setYTextureURL(QString yTextureURL) override;
virtual void setZTextureURL(QString zTextureURL) override;
virtual bool addToScene(EntityItemPointer self,
std::shared_ptr<render::Scene> scene,
render::PendingChanges& pendingChanges);
render::PendingChanges& pendingChanges) override;
virtual void removeFromScene(EntityItemPointer self,
std::shared_ptr<render::Scene> scene,
render::PendingChanges& pendingChanges);
render::PendingChanges& pendingChanges) override;
virtual void setXNNeighborID(const EntityItemID& xNNeighborID);
virtual void setYNNeighborID(const EntityItemID& yNNeighborID);
virtual void setZNNeighborID(const EntityItemID& zNNeighborID);
virtual void setXNNeighborID(const EntityItemID& xNNeighborID) override;
virtual void setYNNeighborID(const EntityItemID& yNNeighborID) override;
virtual void setZNNeighborID(const EntityItemID& zNNeighborID) override;
virtual void setXPNeighborID(const EntityItemID& xPNeighborID);
virtual void setYPNeighborID(const EntityItemID& yPNeighborID);
virtual void setZPNeighborID(const EntityItemID& zPNeighborID);
virtual void setXPNeighborID(const EntityItemID& xPNeighborID) override;
virtual void setYPNeighborID(const EntityItemID& yPNeighborID) override;
virtual void setZPNeighborID(const EntityItemID& zPNeighborID) override;
virtual void updateRegistrationPoint(const glm::vec3& value);
virtual void updateRegistrationPoint(const glm::vec3& value) override;
void setVoxelsFromData(QByteArray uncompressedData, quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize);
void forEachVoxelValue(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize,
@ -131,6 +131,9 @@ public:
void setVolDataDirty() { withWriteLock([&] { _volDataDirty = true; }); }
// Transparent polyvox didn't seem to be working so disable for now
bool isTransparent() override { return false; }
private:
// The PolyVoxEntityItem class has _voxelData which contains dimensions and compressed voxel data. The dimensions
// may not match _voxelVolumeSize.
@ -161,7 +164,7 @@ private:
// these are run off the main thread
void decompressVolumeData();
void compressVolumeDataAndSendEditPacket();
virtual void getMesh(); // recompute mesh
virtual void getMesh() override; // recompute mesh
void computeShapeInfoWorker();
// these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID

View file

@ -40,7 +40,6 @@ static std::array<GeometryCache::Shape, entity::NUM_SHAPES> MAPPING { {
GeometryCache::Cylinder,
} };
RenderableShapeEntityItem::Pointer RenderableShapeEntityItem::baseFactory(const EntityItemID& entityID, const EntityItemProperties& properties) {
Pointer entity = std::make_shared<RenderableShapeEntityItem>(entityID);
entity->setProperties(properties);
@ -72,20 +71,26 @@ void RenderableShapeEntityItem::setUserData(const QString& value) {
}
}
bool RenderableShapeEntityItem::isTransparent() {
if (_procedural && _procedural->isFading()) {
float isFading = Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) < 1.0f;
_procedural->setIsFading(isFading);
return isFading;
} else {
return getLocalRenderAlpha() < 1.0f || EntityItem::isTransparent();
}
}
void RenderableShapeEntityItem::render(RenderArgs* args) {
PerformanceTimer perfTimer("RenderableShapeEntityItem::render");
//Q_ASSERT(getType() == EntityTypes::Shape);
Q_ASSERT(args->_batch);
checkFading();
if (!_procedural) {
_procedural.reset(new Procedural(getUserData()));
_procedural->_vertexSource = simple_vert;
_procedural->_fragmentSource = simple_frag;
_procedural->_state->setCullMode(gpu::State::CULL_NONE);
_procedural->_state->setDepthTest(true, true, gpu::LESS_EQUAL);
_procedural->_state->setBlendFunction(false,
gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
}
gpu::Batch& batch = *args->_batch;
@ -102,14 +107,17 @@ void RenderableShapeEntityItem::render(RenderArgs* args) {
if (_procedural->ready()) {
_procedural->prepare(batch, getPosition(), getDimensions(), getOrientation());
auto outColor = _procedural->getColor(color);
outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f;
batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a);
DependencyManager::get<GeometryCache>()->renderShape(batch, MAPPING[_shape]);
} else {
// FIXME, support instanced multi-shape rendering using multidraw indirect
DependencyManager::get<GeometryCache>()->renderSolidShapeInstance(batch, MAPPING[_shape], color);
color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
auto geometryCache = DependencyManager::get<GeometryCache>();
auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline();
geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline);
}
static const auto triCount = DependencyManager::get<GeometryCache>()->getShapeTriangleCount(MAPPING[_shape]);
args->_details._trianglesRendered += (int)triCount;
}

View file

@ -26,10 +26,12 @@ public:
void render(RenderArgs* args) override;
void setUserData(const QString& value) override;
SIMPLE_RENDERABLE();
bool isTransparent() override;
private:
QSharedPointer<Procedural> _procedural;
std::unique_ptr<Procedural> _procedural { nullptr };
SIMPLE_RENDERABLE();
};

View file

@ -15,8 +15,6 @@
#include <PerfStat.h>
#include <Transform.h>
#include "RenderableTextEntityItem.h"
#include "GLMHelpers.h"
@ -29,10 +27,13 @@ EntityItemPointer RenderableTextEntityItem::factory(const EntityItemID& entityID
void RenderableTextEntityItem::render(RenderArgs* args) {
PerformanceTimer perfTimer("RenderableTextEntityItem::render");
Q_ASSERT(getType() == EntityTypes::Text);
checkFading();
static const float SLIGHTLY_BEHIND = -0.005f;
glm::vec4 textColor = glm::vec4(toGlm(getTextColorX()), 1.0f);
glm::vec4 backgroundColor = glm::vec4(toGlm(getBackgroundColorX()), 1.0f);
float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
bool transparent = fadeRatio < 1.0f;
glm::vec4 textColor = glm::vec4(toGlm(getTextColorX()), fadeRatio);
glm::vec4 backgroundColor = glm::vec4(toGlm(getBackgroundColorX()), fadeRatio);
glm::vec3 dimensions = getDimensions();
// Render background
@ -62,7 +63,7 @@ void RenderableTextEntityItem::render(RenderArgs* args) {
batch.setModelTransform(transformToTopLeft);
DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, false, true);
DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, transparent, false, false, true);
DependencyManager::get<GeometryCache>()->renderQuad(batch, minCorner, maxCorner, backgroundColor);
float scale = _lineHeight / _textRenderer->getFontSize();

View file

@ -164,6 +164,8 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) {
}
void RenderableWebEntityItem::render(RenderArgs* args) {
checkFading();
#ifdef WANT_EXTRA_DEBUGGING
{
gpu::Batch& batch = *args->_batch;
@ -181,6 +183,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
if (!buildWebSurface(static_cast<EntityTreeRenderer*>(args->_renderer))) {
return;
}
_fadeStartTime = usecTimestampNow();
#endif
}
@ -207,8 +210,11 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _texture);
}
float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio);
DependencyManager::get<GeometryCache>()->bindSimpleSRGBTexturedUnlitNoTexAlphaProgram(batch);
DependencyManager::get<GeometryCache>()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f));
DependencyManager::get<GeometryCache>()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio));
}
void RenderableWebEntityItem::setSourceUrl(const QString& value) {

View file

@ -39,7 +39,7 @@ void main(void) {
vec3 color = varColor.rgb;
packDeferredFragmentTranslucent(
interpolatedNormal * frontCondition,
texel.a,
texel.a * varColor.a,
polyline.color * texel.rgb,
vec3(0.01, 0.01, 0.01),
10.0);

View file

@ -1,7 +1,7 @@
<@include gpu/Config.slh@>
<$VERSION_HEADER$>
// Generated on <$_SCRIBE_DATE$>
// model.frag
// polyvox.frag
// fragment shader
//
// Created by Seth Alves on 2015-8-3
@ -41,5 +41,13 @@ void main(void) {
vec3 yzDiffuseScaled = yzDiffuse.rgb * abs(worldNormal.x);
vec4 diffuse = vec4(xyDiffuseScaled + xzDiffuseScaled + yzDiffuseScaled, 1.0);
packDeferredFragment(_normal, 1.0, vec3(diffuse), DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, DEFAULT_OCCLUSION, DEFAULT_SCATTERING);
packDeferredFragment(
_normal,
1.0,
vec3(diffuse),
DEFAULT_ROUGHNESS,
DEFAULT_METALLIC,
DEFAULT_EMISSIVE,
DEFAULT_OCCLUSION,
DEFAULT_SCATTERING);
}

View file

@ -44,7 +44,7 @@ out vec4 varColor;
out vec2 varTexcoord;
const int NUM_VERTICES_PER_PARTICLE = 4;
// This ordering ensures that un-rotated particles render upright in the wiewer.
// This ordering ensures that un-rotated particles render upright in the viewer.
const vec4 UNIT_QUAD[NUM_VERTICES_PER_PARTICLE] = vec4[NUM_VERTICES_PER_PARTICLE](
vec4(-1.0, 1.0, 0.0, 0.0),
vec4(-1.0, -1.0, 0.0, 0.0),

View file

@ -2212,4 +2212,4 @@ void EntityItem::globalizeProperties(EntityItemProperties& properties, const QSt
}
QUuid empty;
properties.setParentID(empty);
}
}

View file

@ -26,6 +26,7 @@
#include <Transform.h>
#include <Sound.h>
#include <SpatiallyNestable.h>
#include <Interpolate.h>
#include "EntityItemID.h"
#include "EntityItemPropertiesDefaults.h"
@ -435,6 +436,7 @@ public:
QUuid getOwningAvatarID() const { return _owningAvatarID; }
void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; }
virtual bool isTransparent() { return _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f : false; }
protected:
@ -565,7 +567,8 @@ protected:
quint64 _lastUpdatedAngularVelocityTimestamp { 0 };
quint64 _lastUpdatedAccelerationTimestamp { 0 };
quint64 _fadeStartTime { usecTimestampNow() };
bool _isFading { true };
};
#endif // hifi_EntityItem_h

View file

@ -634,8 +634,8 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con
}
} else {
// if the entity type doesn't support a detailed intersection, then just return the non-AABox results
// Never intersect with particle effect entities
if (localDistance < distance && EntityTypes::getEntityTypeName(entity->getType()) != "ParticleEffect") {
// Never intersect with particle entities
if (localDistance < distance && entity->getType() != EntityTypes::ParticleEffect) {
distance = localDistance;
face = localFace;
surfaceNormal = localSurfaceNormal;

View file

@ -25,6 +25,7 @@
#include <NumericalConstants.h>
#include <Finally.h>
#include <PathUtils.h>
#include <NetworkAccessManager.h>
#include "OffscreenGLCanvas.h"
#include "GLEscrow.h"
@ -55,6 +56,22 @@ private:
friend class OffscreenQmlSurface;
};
class QmlNetworkAccessManager : public NetworkAccessManager {
public:
friend class QmlNetworkAccessManagerFactory;
protected:
QmlNetworkAccessManager(QObject* parent) : NetworkAccessManager(parent) { }
};
class QmlNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory {
public:
QNetworkAccessManager* create(QObject* parent);
};
QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) {
return new QmlNetworkAccessManager(parent);
}
Q_DECLARE_LOGGING_CATEGORY(offscreenFocus)
Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus")
@ -402,6 +419,8 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
// Create a QML engine.
_qmlEngine = new QQmlEngine;
_qmlEngine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory);
auto importList = _qmlEngine->importPathList();
importList.insert(importList.begin(), PathUtils::resourcesPath());
_qmlEngine->setImportPathList(importList);

View file

@ -596,7 +596,7 @@ int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindin
}
Element element(SCALAR, gpu::UINT32, gpu::UNIFORM_BUFFER);
buffers.insert(Shader::Slot(name, binding, element, Resource::BUFFER));
buffers.insert(Shader::Slot(name, binding, element, Resource::BUFFER, size));
}
return buffersCount;
}

View file

@ -53,19 +53,22 @@ public:
int32 _location{INVALID_LOCATION};
Element _element;
uint16 _resourceType{Resource::BUFFER};
uint32 _size { 0 };
Slot(const Slot& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType) {}
Slot(Slot&& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType) {}
Slot(const std::string& name, int32 location, const Element& element, uint16 resourceType = Resource::BUFFER) :
_name(name), _location(location), _element(element), _resourceType(resourceType) {}
Slot(const Slot& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType), _size(s._size) {}
Slot(Slot&& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType), _size(s._size) {}
Slot(const std::string& name, int32 location, const Element& element, uint16 resourceType = Resource::BUFFER, uint32 size = 0) :
_name(name), _location(location), _element(element), _resourceType(resourceType), _size(size) {}
Slot(const std::string& name) : _name(name) {}
Slot& operator= (const Slot& s) {
_name = s._name;
_location = s._location;
_element = s._element;
_resourceType = s._resourceType;
return (*this); }
_size = s._size;
return (*this);
}
};
class Binding {

View file

@ -700,8 +700,6 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector<
return false;
}
const float UCHAR_TO_FLOAT = 1.0f / float(std::numeric_limits<unsigned char>::max());
// for each face of cube texture
for(int face=0; face < gpu::Texture::NUM_CUBE_FACES; face++) {
@ -788,12 +786,9 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector<
uint pixOffsetIndex = (x + y * width) * numComponents;
// get color from texture and map to range [0, 1]
glm::vec3 clr(float(data[pixOffsetIndex]) * UCHAR_TO_FLOAT,
float(data[pixOffsetIndex+1]) * UCHAR_TO_FLOAT,
float(data[pixOffsetIndex+2]) * UCHAR_TO_FLOAT);
// Gamma correct
clr = ColorUtils::sRGBToLinearVec3(clr);
glm::vec3 clr(ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex]),
ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex + 1]),
ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex + 2]));
// scale color and add to previously accumulated coefficients
sphericalHarmonicsScale(shBuffB.data(), order,

View file

@ -404,6 +404,7 @@ void GeometryResourceWatcher::resourceFinished(bool success) {
if (success) {
_geometryRef = std::make_shared<Geometry>(*_resource);
}
emit finished(success);
}
void GeometryResourceWatcher::resourceRefreshed() {

View file

@ -111,6 +111,9 @@ public:
QUrl getURL() const { return (bool)_resource ? _resource->getURL() : QUrl(); }
signals:
void finished(bool success);
private:
void startWatching();
void stopWatching();

View file

@ -171,7 +171,8 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const
}
NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type) {
NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type,
const QVariantMap& options = QVariantMap()) {
using Type = NetworkTexture;
switch (type) {
@ -188,7 +189,11 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t
break;
}
case Type::CUBE_TEXTURE: {
return model::TextureUsage::createCubeTextureFromImage;
if (options.value("generateIrradiance", true).toBool()) {
return model::TextureUsage::createCubeTextureFromImage;
} else {
return model::TextureUsage::createCubeTextureFromImageWithoutIrradiance;
}
break;
}
case Type::BUMP_TEXTURE: {
@ -225,9 +230,9 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t
}
/// Returns a texture version of an image file
gpu::TexturePointer TextureCache::getImageTexture(const QString& path, Type type) {
gpu::TexturePointer TextureCache::getImageTexture(const QString& path, Type type, QVariantMap options) {
QImage image = QImage(path);
auto loader = getTextureLoaderForType(type);
auto loader = getTextureLoaderForType(type, options);
return gpu::TexturePointer(loader(image, QUrl::fromLocalFile(path).fileName().toStdString()));
}

View file

@ -122,7 +122,7 @@ public:
const gpu::TexturePointer& getNormalFittingTexture();
/// Returns a texture version of an image file
static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE);
static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE, QVariantMap options = QVariantMap());
/// Loads a texture from the specified URL.
NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE,

View file

@ -59,31 +59,28 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val
const uint8 OPAQUE_ALPHA = 255;
const uint8 TRANSPARENT_ALPHA = 0;
if (image.hasAlphaChannel()) {
std::map<uint8, uint32> alphaHistogram;
if (image.format() != QImage::Format_ARGB32) {
image = image.convertToFormat(QImage::Format_ARGB32);
}
// Actual alpha channel? create the histogram
for (int y = 0; y < image.height(); ++y) {
const QRgb* data = reinterpret_cast<const QRgb*>(image.constScanLine(y));
for (int x = 0; x < image.width(); ++x) {
auto alpha = qAlpha(data[x]);
alphaHistogram[alpha] ++;
validAlpha = validAlpha || (alpha != OPAQUE_ALPHA);
// Figure out if we can use a mask for alpha or not
int numOpaques = 0;
int numTranslucents = 0;
const int NUM_PIXELS = image.width() * image.height();
const int MAX_TRANSLUCENT_PIXELS_FOR_ALPHAMASK = (int)(0.05f * (float)(NUM_PIXELS));
const QRgb* data = reinterpret_cast<const QRgb*>(image.constBits());
for (int i = 0; i < NUM_PIXELS; ++i) {
auto alpha = qAlpha(data[i]);
if (alpha == OPAQUE_ALPHA) {
numOpaques++;
} else if (alpha != TRANSPARENT_ALPHA) {
if (++numTranslucents > MAX_TRANSLUCENT_PIXELS_FOR_ALPHAMASK) {
alphaAsMask = false;
break;
}
}
}
// If alpha was meaningfull refine
if (validAlpha && (alphaHistogram.size() > 1)) {
auto totalNumPixels = image.height() * image.width();
auto numOpaques = alphaHistogram[OPAQUE_ALPHA];
auto numTransparents = alphaHistogram[TRANSPARENT_ALPHA];
auto numTranslucents = totalNumPixels - numOpaques - numTransparents;
alphaAsMask = ((numTranslucents / (double)totalNumPixels) < 0.05);
}
validAlpha = (numOpaques != NUM_PIXELS);
}
if (!validAlpha && image.format() != QImage::Format_RGB888) {
@ -660,13 +657,12 @@ const CubeLayout CubeLayout::CUBEMAP_LAYOUTS[] = {
const int CubeLayout::NUM_CUBEMAP_LAYOUTS = sizeof(CubeLayout::CUBEMAP_LAYOUTS) / sizeof(CubeLayout);
gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance) {
bool validAlpha = false;
bool alphaAsMask = true;
QImage image = process2DImageColor(srcImage, validAlpha, alphaAsMask);
gpu::Texture* theTexture = nullptr;
if ((image.width() > 0) && (image.height() > 0)) {
if ((srcImage.width() > 0) && (srcImage.height() > 0)) {
QImage image = srcImage;
if (image.format() != QImage::Format_RGB888) {
image = image.convertToFormat(QImage::Format_RGB888);
}
gpu::Element formatGPU;
gpu::Element formatMip;
@ -674,7 +670,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm
// Find the layout of the cubemap in the 2D image
int foundLayout = CubeLayout::findLayout(image.width(), image.height());
std::vector<QImage> faces;
// If found, go extract the faces as separate images
if (foundLayout >= 0) {
@ -729,3 +725,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm
gpu::Texture* TextureUsage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) {
return processCubeTextureColorFromImage(srcImage, srcImageName, false, true, true, true);
}
gpu::Texture* TextureUsage::createCubeTextureFromImageWithoutIrradiance(const QImage& srcImage, const std::string& srcImageName) {
return processCubeTextureColorFromImage(srcImage, srcImageName, false, true, true, false);
}

View file

@ -40,6 +40,7 @@ public:
static gpu::Texture* createRoughnessTextureFromGlossImage(const QImage& image, const std::string& srcImageName);
static gpu::Texture* createMetallicTextureFromImage(const QImage& image, const std::string& srcImageName);
static gpu::Texture* createCubeTextureFromImage(const QImage& image, const std::string& srcImageName);
static gpu::Texture* createCubeTextureFromImageWithoutIrradiance(const QImage& image, const std::string& srcImageName);
static gpu::Texture* createLightmapTextureFromImage(const QImage& image, const std::string& srcImageName);

View file

@ -200,8 +200,9 @@ void AccountManager::sendRequest(const QString& path,
const JSONCallbackParameters& callbackParams,
const QByteArray& dataByteArray,
QHttpMultiPart* dataMultiPart,
const QVariantMap& propertyMap) {
const QVariantMap& propertyMap,
QUrlQuery query) {
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, "sendRequest",
Q_ARG(const QString&, path),
@ -213,9 +214,9 @@ void AccountManager::sendRequest(const QString& path,
Q_ARG(QVariantMap, propertyMap));
return;
}
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter());
@ -224,13 +225,17 @@ void AccountManager::sendRequest(const QString& path,
uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit());
QUrl requestURL = _authURL;
if (path.startsWith("/")) {
requestURL.setPath(path);
} else {
requestURL.setPath("/" + path);
}
if (!query.isEmpty()) {
requestURL.setQuery(query);
}
if (authType != AccountManagerAuth::None ) {
if (hasValidAccessToken()) {
networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER,
@ -241,22 +246,21 @@ void AccountManager::sendRequest(const QString& path,
<< path << "that requires authentication";
return;
}
}
}
networkRequest.setUrl(requestURL);
if (VERBOSE_HTTP_REQUEST_DEBUGGING) {
qCDebug(networking) << "Making a request to" << qPrintable(requestURL.toString());
if (!dataByteArray.isEmpty()) {
qCDebug(networking) << "The POST/PUT body -" << QString(dataByteArray);
}
}
QNetworkReply* networkReply = NULL;
switch (operation) {
case QNetworkAccessManager::GetOperation:
networkReply = networkAccessManager.get(networkRequest);
@ -269,7 +273,7 @@ void AccountManager::sendRequest(const QString& path,
} else {
networkReply = networkAccessManager.put(networkRequest, dataMultiPart);
}
// make sure dataMultiPart is destroyed when the reply is
connect(networkReply, &QNetworkReply::destroyed, dataMultiPart, &QHttpMultiPart::deleteLater);
} else {
@ -280,7 +284,7 @@ void AccountManager::sendRequest(const QString& path,
networkReply = networkAccessManager.put(networkRequest, dataByteArray);
}
}
break;
case QNetworkAccessManager::DeleteOperation:
networkReply = networkAccessManager.sendCustomRequest(networkRequest, "DELETE");
@ -289,7 +293,7 @@ void AccountManager::sendRequest(const QString& path,
// other methods not yet handled
break;
}
if (networkReply) {
if (!propertyMap.isEmpty()) {
// we have properties to set on the reply so the user can check them after
@ -297,18 +301,18 @@ void AccountManager::sendRequest(const QString& path,
networkReply->setProperty(qPrintable(propertyKey), propertyMap.value(propertyKey));
}
}
if (!callbackParams.isEmpty()) {
// if we have information for a callback, insert the callbackParams into our local map
_pendingCallbackMap.insert(networkReply, callbackParams);
if (callbackParams.updateReciever && !callbackParams.updateSlot.isEmpty()) {
callbackParams.updateReciever->connect(networkReply, SIGNAL(uploadProgress(qint64, qint64)),
callbackParams.updateSlot.toStdString().c_str());
}
}
// if we ended up firing of a request, hook up to it now
connect(networkReply, SIGNAL(finished()), SLOT(processReply()));
}

View file

@ -16,6 +16,7 @@
#include <QtCore/QObject>
#include <QtCore/QUrl>
#include <QtNetwork/QNetworkReply>
#include <QUrlQuery>
#include "NetworkAccessManager.h"
@ -67,7 +68,8 @@ public:
const JSONCallbackParameters& callbackParams = JSONCallbackParameters(),
const QByteArray& dataByteArray = QByteArray(),
QHttpMultiPart* dataMultiPart = NULL,
const QVariantMap& propertyMap = QVariantMap());
const QVariantMap& propertyMap = QVariantMap(),
QUrlQuery query = QUrlQuery());
void setIsAgent(bool isAgent) { _isAgent = isAgent; }

View file

@ -33,12 +33,7 @@ bool AssetResourceRequest::urlIsAssetHash() const {
}
void AssetResourceRequest::doSend() {
auto parts = _url.path().split(".", QString::SkipEmptyParts);
auto hash = parts.length() > 0 ? parts[0] : "";
auto extension = parts.length() > 1 ? parts[1] : "";
// We'll either have a hash or an ATP path to a file (that maps to a hash)
if (urlIsAssetHash()) {
// We've detected that this is a hash - simply use AssetClient to request that asset
auto parts = _url.path().split(".", QString::SkipEmptyParts);

View file

@ -0,0 +1,88 @@
//
// AtpReply.cpp
// libraries/networking/src
//
// Created by Zander Otavka on 8/4/16.
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "ResourceManager.h"
#include "AtpReply.h"
AtpReply::AtpReply(const QUrl& url, QObject* parent) :
_resourceRequest(ResourceManager::createResourceRequest(parent, url)) {
setOperation(QNetworkAccessManager::GetOperation);
connect(_resourceRequest, &AssetResourceRequest::progress, this, &AtpReply::downloadProgress);
connect(_resourceRequest, &AssetResourceRequest::finished, this, &AtpReply::handleRequestFinish);
_resourceRequest->send();
}
AtpReply::~AtpReply() {
if (_resourceRequest) {
_resourceRequest->deleteLater();
_resourceRequest = nullptr;
}
}
qint64 AtpReply::bytesAvailable() const {
return _content.size() - _readOffset + QIODevice::bytesAvailable();
}
qint64 AtpReply::readData(char* data, qint64 maxSize) {
if (_readOffset < _content.size()) {
qint64 readSize = qMin(maxSize, _content.size() - _readOffset);
memcpy(data, _content.constData() + _readOffset, readSize);
_readOffset += readSize;
return readSize;
} else {
return -1;
}
}
void AtpReply::handleRequestFinish() {
Q_ASSERT(_resourceRequest->getState() == ResourceRequest::State::Finished);
switch (_resourceRequest->getResult()) {
case ResourceRequest::Result::Success:
setError(NoError, "Success");
_content = _resourceRequest->getData();
break;
case ResourceRequest::Result::InvalidURL:
setError(ContentNotFoundError, "Invalid URL");
break;
case ResourceRequest::Result::NotFound:
setError(ContentNotFoundError, "Not found");
break;
case ResourceRequest::Result::ServerUnavailable:
setError(ServiceUnavailableError, "Service unavailable");
break;
case ResourceRequest::Result::AccessDenied:
setError(ContentAccessDenied, "Access denied");
break;
case ResourceRequest::Result::Timeout:
setError(TimeoutError, "Timeout");
break;
default:
setError(UnknownNetworkError, "Unknown error");
break;
}
open(ReadOnly | Unbuffered);
setHeader(QNetworkRequest::ContentLengthHeader, QVariant(_content.size()));
if (error() != NoError) {
emit error(error());
}
setFinished(true);
emit readyRead();
emit finished();
_resourceRequest->deleteLater();
_resourceRequest = nullptr;
}

View file

@ -0,0 +1,40 @@
//
// AtpReply.h
// libraries/networking/src
//
// Created by Zander Otavka on 8/4/16.
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_AtpReply_h
#define hifi_AtpReply_h
#include <QtNetwork/QNetworkReply>
#include <QUrl>
#include "AssetResourceRequest.h"
class AtpReply : public QNetworkReply {
Q_OBJECT
public:
AtpReply(const QUrl& url, QObject* parent = Q_NULLPTR);
~AtpReply();
qint64 bytesAvailable() const override;
void abort() override { }
bool isSequential() const override { return true; }
protected:
qint64 readData(char* data, qint64 maxSize) override;
private:
void handleRequestFinish();
ResourceRequest* _resourceRequest { nullptr };
QByteArray _content;
qint64 _readOffset { 0 };
};
#endif // hifi_AtpReply_h

View file

@ -0,0 +1,36 @@
//
// GroupRank.h
// libraries/networking/src/
//
// Created by Seth Alves on 2016-7-21.
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_GroupRank_h
#define hifi_GroupRank_h
class GroupRank {
public:
GroupRank() {}
GroupRank(QUuid id, unsigned int order, QString name, unsigned int membersCount) :
id(id), order(order), name(name), membersCount(membersCount) {}
QUuid id;
int order { -1 };
QString name;
int membersCount { -1 };
};
inline bool operator==(const GroupRank& lhs, const GroupRank& rhs) {
return
lhs.id == rhs.id &&
lhs.order == rhs.order &&
lhs.name == rhs.name &&
lhs.membersCount == rhs.membersCount;
}
inline bool operator!=(const GroupRank& lhs, const GroupRank& rhs) { return !(lhs == rhs); }
#endif // hifi_GroupRank_h

View file

@ -135,17 +135,25 @@ void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) {
_permissions = newPermissions;
if (originalPermissions.canAdjustLocks != newPermissions.canAdjustLocks) {
emit isAllowedEditorChanged(_permissions.canAdjustLocks);
if (originalPermissions.can(NodePermissions::Permission::canAdjustLocks) !=
newPermissions.can(NodePermissions::Permission::canAdjustLocks)) {
emit isAllowedEditorChanged(_permissions.can(NodePermissions::Permission::canAdjustLocks));
}
if (originalPermissions.canRezPermanentEntities != newPermissions.canRezPermanentEntities) {
emit canRezChanged(_permissions.canRezPermanentEntities);
if (originalPermissions.can(NodePermissions::Permission::canRezPermanentEntities) !=
newPermissions.can(NodePermissions::Permission::canRezPermanentEntities)) {
emit canRezChanged(_permissions.can(NodePermissions::Permission::canRezPermanentEntities));
}
if (originalPermissions.canRezTemporaryEntities != newPermissions.canRezTemporaryEntities) {
emit canRezTmpChanged(_permissions.canRezTemporaryEntities);
if (originalPermissions.can(NodePermissions::Permission::canRezTemporaryEntities) !=
newPermissions.can(NodePermissions::Permission::canRezTemporaryEntities)) {
emit canRezTmpChanged(_permissions.can(NodePermissions::Permission::canRezTemporaryEntities));
}
if (originalPermissions.canWriteToAssetServer != newPermissions.canWriteToAssetServer) {
emit canWriteAssetsChanged(_permissions.canWriteToAssetServer);
if (originalPermissions.can(NodePermissions::Permission::canWriteToAssetServer) !=
newPermissions.can(NodePermissions::Permission::canWriteToAssetServer)) {
emit canWriteAssetsChanged(_permissions.can(NodePermissions::Permission::canWriteToAssetServer));
}
if (originalPermissions.can(NodePermissions::Permission::canKick) !=
newPermissions.can(NodePermissions::Permission::canKick)) {
emit canKickChanged(_permissions.can(NodePermissions::Permission::canKick));
}
}

View file

@ -106,10 +106,11 @@ public:
void setSessionUUID(const QUuid& sessionUUID);
void setPermissions(const NodePermissions& newPermissions);
bool isAllowedEditor() const { return _permissions.canAdjustLocks; }
bool getThisNodeCanRez() const { return _permissions.canRezPermanentEntities; }
bool getThisNodeCanRezTmp() const { return _permissions.canRezTemporaryEntities; }
bool getThisNodeCanWriteAssets() const { return _permissions.canWriteToAssetServer; }
bool isAllowedEditor() const { return _permissions.can(NodePermissions::Permission::canAdjustLocks); }
bool getThisNodeCanRez() const { return _permissions.can(NodePermissions::Permission::canRezPermanentEntities); }
bool getThisNodeCanRezTmp() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryEntities); }
bool getThisNodeCanWriteAssets() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); }
bool getThisNodeCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); }
quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); }
QUdpSocket& getDTLSSocket();
@ -258,6 +259,7 @@ signals:
void canRezChanged(bool canRez);
void canRezTmpChanged(bool canRezTmp);
void canWriteAssetsChanged(bool canWriteAssets);
void canKickChanged(bool canKick);
protected slots:
void connectedForLocalSocketTest();

View file

@ -1,6 +1,6 @@
//
// NetworkAccessManager.cpp
//
// libraries/networking/src
//
// Created by Clement on 7/1/14.
// Copyright 2014 High Fidelity, Inc.
@ -11,6 +11,7 @@
#include <QThreadStorage>
#include "AtpReply.h"
#include "NetworkAccessManager.h"
QThreadStorage<QNetworkAccessManager*> networkAccessManagers;
@ -23,3 +24,13 @@ QNetworkAccessManager& NetworkAccessManager::getInstance() {
return *networkAccessManagers.localData();
}
QNetworkReply* NetworkAccessManager::createRequest(Operation operation, const QNetworkRequest& request, QIODevice* device) {
if (request.url().scheme() == "atp" && operation == GetOperation) {
return new AtpReply(request.url());
//auto url = request.url().toString();
//return QNetworkAccessManager::createRequest(operation, request, device);
} else {
return QNetworkAccessManager::createRequest(operation, request, device);
}
}

View file

@ -1,6 +1,6 @@
//
// NetworkAccessManager.h
//
// libraries/networking/src
//
// Created by Clement on 7/1/14.
// Copyright 2014 High Fidelity, Inc.
@ -13,12 +13,17 @@
#define hifi_NetworkAccessManager_h
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtQml/QQmlNetworkAccessManagerFactory>
/// Wrapper around QNetworkAccessManager to restrict at one instance by thread
class NetworkAccessManager : public QObject {
class NetworkAccessManager : public QNetworkAccessManager {
Q_OBJECT
public:
static QNetworkAccessManager& getInstance();
protected:
NetworkAccessManager(QObject* parent = Q_NULLPTR) : QNetworkAccessManager(parent) {}
virtual QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* device = Q_NULLPTR) override;
};
#endif // hifi_NetworkAccessManager_h

View file

@ -65,10 +65,11 @@ public:
void setPermissions(const NodePermissions& newPermissions) { _permissions = newPermissions; }
NodePermissions getPermissions() const { return _permissions; }
bool isAllowedEditor() const { return _permissions.canAdjustLocks; }
bool getCanRez() const { return _permissions.canRezPermanentEntities; }
bool getCanRezTmp() const { return _permissions.canRezTemporaryEntities; }
bool getCanWriteToAssetServer() const { return _permissions.canWriteToAssetServer; }
bool isAllowedEditor() const { return _permissions.can(NodePermissions::Permission::canAdjustLocks); }
bool getCanRez() const { return _permissions.can(NodePermissions::Permission::canRezPermanentEntities); }
bool getCanRezTmp() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryEntities); }
bool getCanWriteToAssetServer() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); }
bool getCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); }
void parseIgnoreRequestMessage(QSharedPointer<ReceivedMessage> message);
void addIgnoredNode(const QUuid& otherNodeID);

View file

@ -283,12 +283,7 @@ void NodeList::sendDomainServerCheckIn() {
auto accountManager = DependencyManager::get<AccountManager>();
const QUuid& connectionToken = _domainHandler.getConnectionToken();
// we assume that we're on the same box as the DS if it has the same local address and
// it didn't present us with a connection token to use for username signature
bool localhostDomain = _domainHandler.getSockAddr().getAddress() == QHostAddress::LocalHost
|| (_domainHandler.getSockAddr().getAddress() == _localSockAddr.getAddress() && connectionToken.isNull());
bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull() && !localhostDomain;
bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull();
if (requiresUsernameSignature && !accountManager->getAccountInfo().hasPrivateKey()) {
qWarning() << "A keypair is required to present a username signature to the domain-server"
@ -732,7 +727,7 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) {
emit ignoredNode(nodeID);
} else {
qWarning() << "UsersScriptingInterface::ignore called with an invalid ID or an ID which matches the current session ID.";
qWarning() << "NodeList::ignoreNodeBySessionID called with an invalid ID or an ID which matches the current session ID.";
}
}
@ -764,3 +759,28 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) {
}
}
}
void NodeList::kickNodeBySessionID(const QUuid& nodeID) {
// send a request to domain-server to kick the node with the given session ID
// the domain-server will handle the persistence of the kick (via username or IP)
if (!nodeID.isNull() && _sessionUUID != nodeID ) {
if (getThisNodeCanKick()) {
// setup the packet
auto kickPacket = NLPacket::create(PacketType::NodeKickRequest, NUM_BYTES_RFC4122_UUID, true);
// write the node ID to the packet
kickPacket->write(nodeID.toRfc4122());
qDebug() << "Sending packet to kick node" << uuidStringWithoutCurlyBraces(nodeID);
sendPacket(std::move(kickPacket), _domainHandler.getSockAddr());
} else {
qWarning() << "You do not have permissions to kick in this domain."
<< "Request to kick node" << uuidStringWithoutCurlyBraces(nodeID) << "will not be sent";
}
} else {
qWarning() << "NodeList::kickNodeBySessionID called with an invalid ID or an ID which matches the current session ID.";
}
}

Some files were not shown because too many files have changed in this diff Show more