Merge branch 'master' into feature/qt-update-misc

This commit is contained in:
David Rowe 2021-04-10 09:10:26 +12:00
commit c8632f8613
116 changed files with 2175 additions and 643 deletions

17
.github/stale.yml vendored Normal file
View file

@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 120
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 60
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had recent activity.
It will be closed if no further activity occurs.
Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View file

@ -8,6 +8,7 @@ if (APPLE)
endif ()
setup_memory_debugger()
setup_thread_debugger()
# link in the shared libraries
link_hifi_libraries(

View file

@ -269,7 +269,13 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
// the avatar mixer uses the negative value of the sent version
instanceVersionRef = -packetTraitVersion;
} else {
_avatar->processTraitInstance(traitType, instanceID, message.read(traitSize));
// Don't accept avatar entity data for distribution unless sender has rez permissions on the domain.
// The sender shouldn't be sending avatar entity data, however this provides a back-up.
auto trait = message.read(traitSize);
if (sendingNode.getCanRezAvatarEntities()) {
_avatar->processTraitInstance(traitType, instanceID, trait);
}
instanceVersionRef = packetTraitVersion;
}
@ -290,6 +296,29 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
}
}
void AvatarMixerClientData::emulateDeleteEntitiesTraitsMessage(const QList<QUuid>& avatarEntityIDs) {
// Emulates processSetTraitsMessage() actions on behalf of an avatar whose canRezAvatarEntities permission has been removed.
// The source avatar should be removing its avatar entities. However, using this method provides a back-up.
auto traitType = AvatarTraits::AvatarEntity;
for (const auto& entityID : avatarEntityIDs) {
auto& instanceVersionRef = _lastReceivedTraitVersions.getInstanceValueRef(traitType, entityID);
_avatar->processDeletedTraitInstance(traitType, entityID);
// Mixer doesn't need deleted IDs.
_avatar->getAndClearRecentlyRemovedIDs();
// to track a deleted instance but keep version information
// the avatar mixer uses the negative value of the sent version
// Because there is no originating message from an avatar we enlarge the magnitude by 1.
// If a user subsequently has canRezAvatarEntities permission granted, they will have to relog in order for their
// avatar entities to be visible to others.
instanceVersionRef = -instanceVersionRef - 1;
}
_lastReceivedTraitsChange = std::chrono::steady_clock::now();
}
void AvatarMixerClientData::processBulkAvatarTraitsAckMessage(ReceivedMessage& message) {
// Avatar Traits flow control marks each outgoing avatar traits packet with a
// sequence number. The mixer caches the traits sent in the traits packet.

View file

@ -132,6 +132,7 @@ public:
int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed
void processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode);
void emulateDeleteEntitiesTraitsMessage(const QList<QUuid>& avatarEntityIDs);
void processBulkAvatarTraitsAckMessage(ReceivedMessage& message);
void checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData, Node& sendingNode,
AvatarTraits::TraitVersion traitVersion);

View file

@ -432,6 +432,17 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
}
}
// The source avatar should be removing its avatar entities. However, provide a back-up.
if (sendAvatar) {
if (!sourceAvatarNode->getCanRezAvatarEntities()) {
auto sourceAvatarNodeData = reinterpret_cast<AvatarMixerClientData*>(sourceAvatarNode->getLinkedData());
auto avatarEntityIDs = sourceAvatarNodeData->getAvatar().getAvatarEntityIDs();
if (avatarEntityIDs.count() > 0) {
sourceAvatarNodeData->emulateDeleteEntitiesTraitsMessage(avatarEntityIDs);
}
}
}
if (sendAvatar) {
AvatarDataSequenceNumber lastSeqToReceiver = destinationNodeData->getLastBroadcastSequenceNumber(sourceAvatarNode->getLocalID());
AvatarDataSequenceNumber lastSeqFromSender = sourceAvatarNodeData->getLastReceivedSequenceNumber();

View file

@ -398,7 +398,7 @@ void ScriptableAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityDa
// clear deleted traits
for (const auto& id : idsToClear) {
clearAvatarEntity(id);
clearAvatarEntityInternal(id);
}
}
@ -408,7 +408,7 @@ void ScriptableAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArra
std::map<QUuid, EntityItemPointer>::iterator itr = _entities.find(entityID);
if (itr != _entities.end()) {
_entities.erase(itr);
clearAvatarEntity(entityID);
clearAvatarEntityInternal(entityID);
}
return;
}

View file

@ -26,5 +26,6 @@ function(LINK_HIFI_LIBRARIES)
endforeach()
setup_memory_debugger()
setup_thread_debugger()
endfunction()

View file

@ -8,23 +8,34 @@
#
macro(SETUP_MEMORY_DEBUGGER)
if (DEFINED ENV{HIFI_MEMORY_DEBUGGING})
SET( HIFI_MEMORY_DEBUGGING true )
if ("$ENV{VIRCADIA_MEMORY_DEBUGGING}")
if (VIRCADIA_THREAD_DEBUGGING)
message(FATAL_ERROR "Thread debugging and memory debugging can't be enabled at the same time." )
endif()
SET( VIRCADIA_MEMORY_DEBUGGING true )
endif ()
if (HIFI_MEMORY_DEBUGGING)
if (VIRCADIA_MEMORY_DEBUGGING)
if (UNIX)
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
# for clang on Linux
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer -shared-libasan -fsanitize=undefined -fsanitize=address -fsanitize-recover=address")
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -shared-libasan -fsanitize=undefined -fsanitize=address -fsanitize-recover=address")
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -shared-libasan -fsanitize=undefined -fsanitize=address -fsanitize-recover=address")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer -fsanitize=undefined -fsanitize=address -fsanitize=leak -fsanitize-recover=address")
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -fsanitize-recover=address")
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -fsanitize-recover=address")
else ()
# for gcc on Linux
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined -fsanitize=address -U_FORTIFY_SOURCE -fno-stack-protector -fno-omit-frame-pointer")
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libasan -static-libstdc++ -fsanitize=undefined -fsanitize=address")
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libasan -static-libstdc++ -fsanitize=undefined -fsanitize=address")
# For some reason, using -fstack-protector results in this error:
# usr/bin/ld: ../../libraries/audio/libaudio.so: undefined reference to `FIR_1x4_AVX512(float*, float*, float*, float*, float*, float (*) [64], int)'
# The '-DSTACK_PROTECTOR' argument below disables the usage of this function in the code. This should be fine as it only works on the latest Intel hardware,
# and is an optimization that should make no functional difference.
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -U_FORTIFY_SOURCE -DSTACK_PROTECTOR -fstack-protector-strong -fno-omit-frame-pointer")
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak ")
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak")
endif()
endif (UNIX)
else()
message(FATAL_ERROR "Memory debugging is not supported on this platform." )
endif()
endif ()
endmacro(SETUP_MEMORY_DEBUGGER)

View file

@ -53,6 +53,7 @@ macro(SETUP_HIFI_LIBRARY)
endforeach()
setup_memory_debugger()
setup_thread_debugger()
# create a library and set the property so it can be referenced later
if (${${TARGET_NAME}_SHARED})

View file

@ -0,0 +1,36 @@
#
# MemoryDebugger.cmake
#
# Copyright 2021 Vircadia Contributors
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
macro(SETUP_THREAD_DEBUGGER)
if ("$ENV{VIRCADIA_THREAD_DEBUGGING}")
if (VIRCADIA_MEMORY_DEBUGGING )
message(FATAL_ERROR "Thread debugging and memory debugging can't be enabled at the same time." )
endif ()
SET(VIRCADIA_THREAD_DEBUGGING true)
endif ()
if (VIRCADIA_THREAD_DEBUGGING)
if (UNIX)
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
# for clang on Linux
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread -fno-omit-frame-pointer")
SET(CMAKE_EXE_LINKER_FLAGS "-fsanitize=thread ${CMAKE_EXE_LINKER_FLAGS}")
SET(CMAKE_SHARED_LINKER_FLAGS "-fsanitize=thread ${CMAKE_EXE_LINKER_FLAGS}")
else ()
# for gcc on Linux
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread -fno-omit-frame-pointer")
SET(CMAKE_EXE_LINKER_FLAGS " -fsanitize=thread ${CMAKE_EXE_LINKER_FLAGS}")
SET(CMAKE_SHARED_LINKER_FLAGS "-fsanitize=thread ${CMAKE_EXE_LINKER_FLAGS}")
endif()
else()
message(FATAL_ERROR "Thread debugging is not supported on this platform.")
endif()
endif ()
endmacro(SETUP_THREAD_DEBUGGER)

View file

@ -15,6 +15,7 @@ if (APPLE)
endif ()
setup_memory_debugger()
setup_thread_debugger()
# TODO: find a solution that will handle web file changes in resources on windows without a re-build.
# Currently the resources are only copied on post-build. If one is changed but the domain-server is not, they will

View file

@ -1,5 +1,5 @@
{
"version": 2.4,
"version": 2.5,
"settings": [
{
"name": "metaverse",
@ -295,10 +295,10 @@
},
{
"name": "approved_safe_urls",
"label": "Approved Script and QML URLs",
"label": "Approved Script and QML URLs (Not Enabled)",
"help": "These URLs will be sent to the Interface as safe URLs to allow through the whitelist if the Interface has this security option enabled.",
"placeholder": "0",
"default": "1",
"placeholder": "",
"default": "",
"advanced": false
},
{
@ -338,7 +338,7 @@
"name": "standard_permissions",
"type": "table",
"label": "Domain-Wide User Permissions",
"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>Rez Certified</strong><br />Sets whether a user can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether a user can create new certified 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><li><strong>Rez Certified</strong><br />Sets whether a user can create new certified entities.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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>Avatar Entities</strong><br />Sets whether a user can use avatar entities on 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>Rez Certified</strong><br />Sets whether a user can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether a user can create new certified 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><li><strong>Rez Certified</strong><br />Sets whether a user can create new certified entities.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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": [
@ -347,8 +347,8 @@
"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>Rez Certified</strong><br />Sets whether a user can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether a user can create new certified 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><li><strong>Replace Content</strong><br>Sets whether a user can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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": 11
"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>Avatar Entities</strong><br />Sets whether a user can use avatar entities on 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>Rez Certified</strong><br />Sets whether a user can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether a user can create new certified 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><li><strong>Replace Content</strong><br>Sets whether a user can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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": 12
}
],
"columns": [
@ -363,6 +363,13 @@
"editable": true,
"default": false
},
{
"name": "id_can_rez_avatar_entities",
"label": "Avatar Entities",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
@ -439,17 +446,20 @@
"default": [
{
"id_can_connect": true,
"id_can_rez_avatar_entities": true,
"id_can_rez_tmp_certified": true,
"permissions_id": "anonymous"
},
{
"id_can_connect": true,
"id_can_rez_avatar_entities": true,
"id_can_rez_tmp_certified": true,
"permissions_id": "friends"
},
{
"id_can_adjust_locks": true,
"id_can_connect": true,
"id_can_rez_avatar_entities": true,
"id_can_adjust_locks": true,
"id_can_connect_past_max_capacity": true,
"id_can_kick": true,
"id_can_replace_content": true,
@ -463,6 +473,7 @@
},
{
"id_can_connect": true,
"id_can_rez_avatar_entities": true,
"id_can_rez_tmp_certified": true,
"permissions_id": "logged-in"
}
@ -484,8 +495,8 @@
"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>Rez Temporary</strong><br />Sets whether users in specific groups can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether a users in specific groups can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether a user can create new certified 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><li><strong>Replace Content</strong><br>Sets whether a user can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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": 11
"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>Avatar Entities</strong><br />Sets whether users in specific groups can use avatar entities on 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>Rez Temporary</strong><br />Sets whether users in specific groups can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether users in specific groups can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether users in specific groups can create new certified 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><li><strong>Replace Content</strong><br>Sets whether a user can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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": 12
}
],
"columns": [
@ -525,6 +536,13 @@
"editable": true,
"default": false
},
{
"name": "id_can_rez_avatar_entities",
"label": "Avatar Entities",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
@ -613,8 +631,8 @@
"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>Rez Certified</strong><br />Sets whether a users in specific groups can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether a user can create new certified 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><li><strong>Replace Content</strong><br>Sets whether users in specific groups can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property</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": 11
"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>Avatar Entities</strong><br />Sets whether users in specific groups can use avatar entities on 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>Rez Certified</strong><br />Sets whether users in specific groups can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether users in specific groups can create new certified 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><li><strong>Replace Content</strong><br>Sets whether users in specific groups can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property</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": 12
}
],
"columns": [
@ -651,6 +669,13 @@
"editable": true,
"default": false
},
{
"name": "id_can_rez_avatar_entities",
"label": "Avatar Entities",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
@ -734,8 +759,8 @@
"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>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether a user can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether a user can create new certified 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><li><strong>Replace Content</strong><br>Sets whether a user can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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": 11
"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>Avatar Entities</strong><br />Sets whether a user can use avatar entities on 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>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether a user can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether a user can create new certified 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><li><strong>Replace Content</strong><br>Sets whether a user can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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": 12
}
],
"columns": [
@ -750,6 +775,13 @@
"editable": true,
"default": false
},
{
"name": "id_can_rez_avatar_entities",
"label": "Avatar Entities",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
@ -833,8 +865,8 @@
"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>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether users from specific IPs can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether users from specific IPs can create new certified 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><li><strong>Replace Content</strong><br>Sets whether users from specific IPs can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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": 11
"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>Avatar Entities</strong><br />Sets whether users from specific IPs can use avatar entities on 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>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether users from specific IPs can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether users from specific IPs can create new certified 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><li><strong>Replace Content</strong><br>Sets whether users from specific IPs can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</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": 12
}
],
"columns": [
@ -849,6 +881,13 @@
"editable": true,
"default": false
},
{
"name": "id_can_rez_avatar_entities",
"label": "Avatar Entities",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
@ -932,8 +971,8 @@
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide MAC Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users with specific MACs can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users from specific MACs 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 with specific MACs can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users with specific MACs can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether users with specific MACs can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether users with specific MACs can create new certified entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users with specific MACs can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether users with specific MACs can connect even if the domain has reached or exceeded its maximum allowed agents.</li><li><strong>Replace Content</strong><br>Sets whether users with specific MACs can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</li></ul><p>Note that permissions assigned to a specific MAC will supersede any parameter-level permissions that might otherwise apply to that user (from groups or standard permissions above). MAC address permissions are overriden if the user has their own row in the users section.</p>'>?</a>",
"span": 11
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide MAC Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users with specific MACs can connect to the domain.</li><li><strong>Avatar Entities</strong><br />Sets whether users with specific MACs can use avatar entities on the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users from specific MACs 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 with specific MACs can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users with specific MACs can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether users with specific MACs can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether users with specific MACs can create new certified entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users with specific MACs can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether users with specific MACs can connect even if the domain has reached or exceeded its maximum allowed agents.</li><li><strong>Replace Content</strong><br>Sets whether users with specific MACs can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</li></ul><p>Note that permissions assigned to a specific MAC will supersede any parameter-level permissions that might otherwise apply to that user (from groups or standard permissions above). MAC address permissions are overriden if the user has their own row in the users section.</p>'>?</a>",
"span": 12
}
],
"columns": [
@ -948,6 +987,13 @@
"editable": true,
"default": false
},
{
"name": "id_can_rez_avatar_entities",
"label": "Avatar Entities",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
@ -1031,8 +1077,8 @@
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide Machine Fingerprint Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users with specific Machine Fingerprints can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users from specific Machine Fingerprints 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 with specific Machine Fingerprints can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users with specific Machine Fingerprints can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether users with specific Machine Fingerprints can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether users with specific Machine Fingerprints can create new certified entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users with specific Machine Fingerprints can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether users with specific Machine Fingerprints can connect even if the domain has reached or exceeded its maximum allowed agents.</li><li><strong>Replace Content</strong><br>Sets whether users with specific Machine Fingerprints can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</li></ul><p>Note that permissions assigned to a specific Machine Fingerprint will supersede any parameter-level permissions that might otherwise apply to that user (from groups or standard permissions above). Machine Fingerprint address permissions are overriden if the user has their own row in the users section.</p>'>?</a>",
"span": 11
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide Machine Fingerprint Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users with specific Machine Fingerprints can connect to the domain.</li><li><strong>Avatar Entities</strong><br />Sets whether users with specific Machine Fingerprints can use avatar entities on the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users from specific Machine Fingerprints 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 with specific Machine Fingerprints can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users with specific Machine Fingerprints can create new entities with a finite lifetime.</li><li><strong>Rez Certified</strong><br />Sets whether users with specific Machine Fingerprints can create new certified entities.</li><li><strong>Rez Temporary Certified</strong><br />Sets whether users with specific Machine Fingerprints can create new certified entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users with specific Machine Fingerprints can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether users with specific Machine Fingerprints can connect even if the domain has reached or exceeded its maximum allowed agents.</li><li><strong>Replace Content</strong><br>Sets whether users with specific Machine Fingerprints can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</li></ul><p>Note that permissions assigned to a specific Machine Fingerprint will supersede any parameter-level permissions that might otherwise apply to that user (from groups or standard permissions above). Machine Fingerprint address permissions are overriden if the user has their own row in the users section.</p>'>?</a>",
"span": 12
}
],
"columns": [
@ -1047,6 +1093,13 @@
"editable": true,
"default": false
},
{
"name": "id_can_rez_avatar_entities",
"label": "Avatar Entities",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",

View file

@ -465,6 +465,7 @@ function savePermissions() {
"standard_permissions": [
{
"id_can_connect": anonymousCanConnect,
"id_can_rez_avatar_entities": anonymousCanConnect,
"id_can_rez": anonymousCanRez,
"id_can_rez_certified": anonymousCanRez,
"id_can_rez_tmp": anonymousCanRez,
@ -473,6 +474,7 @@ function savePermissions() {
},
{
"id_can_connect": friendsCanConnect,
"id_can_rez_avatar_entities": friendsCanConnect,
"id_can_rez": friendsCanRez,
"id_can_rez_certified": friendsCanRez,
"id_can_rez_tmp": friendsCanRez,
@ -481,6 +483,7 @@ function savePermissions() {
},
{
"id_can_connect": loggedInCanConnect,
"id_can_rez_avatar_entities": loggedInCanConnect,
"id_can_rez": loggedInCanRez,
"id_can_rez_certified": loggedInCanRez,
"id_can_rez_tmp": loggedInCanRez,
@ -490,6 +493,7 @@ function savePermissions() {
{
"id_can_adjust_locks": localhostPermissions,
"id_can_connect": localhostPermissions,
"id_can_rez_avatar_entities": localhostPermissions,
"id_can_connect_past_max_capacity": localhostPermissions,
"id_can_kick": localhostPermissions,
"id_can_replace_content": localhostPermissions,

View file

@ -353,6 +353,7 @@ void DomainGatekeeper::updateNodePermissions() {
userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer;
userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent;
userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData;
userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities;
} else {
// 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
@ -448,6 +449,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer;
userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent;
userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData;
userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities;
newNode->setPermissions(userPerms);
return newNode;
}

View file

@ -66,6 +66,7 @@ using namespace std::chrono;
Q_LOGGING_CATEGORY(domain_server, "hifi.domain_server")
Q_LOGGING_CATEGORY(domain_server_ice, "hifi.domain_server.ice")
Q_LOGGING_CATEGORY(domain_server_auth, "vircadia.domain_server.auth")
const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token";
const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace";
@ -1545,9 +1546,9 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress, const
static const QString PORT_SETTINGS_KEY = "domain_server." + PUBLIC_SOCKET_PORT_KEY;
const int portFromSettings = _settingsManager.valueForKeyPath(PORT_SETTINGS_KEY).toInt();
if (port != NULL) {
if (port != 0) {
domainObject[PUBLIC_SOCKET_PORT_KEY] = port;
} else if (portFromSettings != NULL) {
} else if (portFromSettings != 0) {
domainObject[PUBLIC_SOCKET_PORT_KEY] = portFromSettings;
}
@ -2771,6 +2772,20 @@ void DomainServer::profileRequestFinished() {
}
}
QString DomainServer::operationToString(const QNetworkAccessManager::Operation &op) {
switch(op) {
case QNetworkAccessManager::Operation::HeadOperation: return "HEAD";
case QNetworkAccessManager::Operation::GetOperation: return "GET";
case QNetworkAccessManager::Operation::PutOperation: return "PUT";
case QNetworkAccessManager::Operation::PostOperation: return "POST";
case QNetworkAccessManager::Operation::DeleteOperation: return "DELETE";
case QNetworkAccessManager::Operation::CustomOperation: return "CUSTOM";
case QNetworkAccessManager::Operation::UnknownOperation:
default:
return "UNKNOWN";
}
}
std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* connection) {
static const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie";
@ -2784,6 +2799,9 @@ std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* c
QVariant adminUsersVariant = _settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY);
QVariant adminRolesVariant = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY);
QString httpPeerAddress = connection->peerAddress().toString();
QString httpOperation = operationToString(connection->requestOperation());
if (_oauthEnable) {
QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY);
@ -2817,11 +2835,15 @@ std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* c
foreach(const QString& userRole, sessionData.getRoles()) {
if (adminRolesArray.contains(userRole)) {
// this user has a role that allows them to administer the domain-server
qCInfo(domain_server_auth) << httpPeerAddress << "- OAuth:" << profileUsername << " - "
<< httpOperation << " " << connection->requestUrl();
return { true, profileUsername };
}
}
}
qCWarning(domain_server_auth) << httpPeerAddress << "- OAuth authentication failed for " << profileUsername << "-"
<< httpOperation << " " << connection->requestUrl();
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY);
// the user does not have allowed username or role, return 401
@ -2833,6 +2855,9 @@ std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* c
if (connection->requestHeader(REQUESTED_WITH_HEADER) == XML_REQUESTED_WITH) {
// unauthorized XHR requests get a 401 and not a 302, since there isn't an XHR
// path to OAuth authorize
qCWarning(domain_server_auth) << httpPeerAddress << "- OAuth unauthorized XHR -"
<< httpOperation << " " << connection->requestUrl();
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY);
} else {
// re-direct this user to OAuth page
@ -2849,6 +2874,8 @@ std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* c
redirectHeaders.insert("Location", authURL.toEncoded());
qCWarning(domain_server_auth) << httpPeerAddress << "- OAuth redirecting -"
<< httpOperation << " " << connection->requestUrl();
connection->respond(HTTPConnection::StatusCode302,
QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders);
}
@ -2883,7 +2910,12 @@ std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* c
"" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex();
if (settingsUsername == headerUsername && hexHeaderPassword == settingsPassword) {
qCInfo(domain_server_auth) << httpPeerAddress << "- Basic:" << headerUsername << "-"
<< httpOperation << " " << connection->requestUrl();
return { true, headerUsername };
} else {
qCWarning(domain_server_auth) << httpPeerAddress << "- Basic auth failed for" << headerUsername << "-"
<< httpOperation << " " << connection->requestUrl();
}
}
}
@ -2904,11 +2936,13 @@ std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* c
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY,
HTTPConnection::DefaultContentType, basicAuthHeader);
qCWarning(domain_server_auth) << httpPeerAddress << "- Basic auth required -" << httpOperation << " " << connection->requestUrl();
// not authenticated, bubble up false
return { false, QString() };
} else {
// we don't have an OAuth URL + admin roles/usernames, so all users are authenticated
qCWarning(domain_server_auth) << httpPeerAddress << "- OPEN ACCESS -" << httpOperation << " " << connection->requestUrl();
return { true, QString() };
}
}

View file

@ -43,6 +43,7 @@
Q_DECLARE_LOGGING_CATEGORY(domain_server)
Q_DECLARE_LOGGING_CATEGORY(domain_server_ice)
Q_DECLARE_LOGGING_CATEGORY(domain_server_auth)
typedef QSharedPointer<Assignment> SharedAssignmentPointer;
typedef QMultiHash<QUuid, WalletTransaction*> TransactionHash;
@ -233,6 +234,8 @@ private:
std::initializer_list<QString> optionalData = { },
bool requireAccessToken = true);
QString operationToString(const QNetworkAccessManager::Operation &op);
SubnetList _acSubnetWhitelist;
std::vector<QString> _replicatedUsernames;

View file

@ -37,6 +37,7 @@
#include <SettingHandle.h>
#include <SettingHelpers.h>
#include <FingerprintUtils.h>
#include <ModerationFlags.h>
#include "DomainServerNodeData.h"
@ -527,6 +528,28 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena
*newAdminRoles = adminRoles;
}
if (oldVersion < 2.5) {
// Default values for new canRezAvatarEntities permission.
unpackPermissions();
std::list<std::unordered_map<NodePermissionsKey, NodePermissionsPointer>> permissionsSets{
_standardAgentPermissions.get(),
_agentPermissions.get(),
_ipPermissions.get(),
_macPermissions.get(),
_machineFingerprintPermissions.get(),
_groupPermissions.get(),
_groupForbiddens.get()
};
foreach (auto permissionsSet, permissionsSets) {
for (auto entry : permissionsSet) {
const auto& userKey = entry.first;
if (permissionsSet[userKey]->can(NodePermissions::Permission::canConnectToDomain)) {
permissionsSet[userKey]->set(NodePermissions::Permission::canRezAvatarEntities);
}
}
}
packPermissions();
}
// write the current description version to our settings
*versionVariant = _descriptionVersion;
@ -863,6 +886,20 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
// pull the UUID being kicked from the packet
QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
bool hasOptionalBanParameters = false;
int banParameters;
bool banByUsername;
bool banByFingerprint;
bool banByIP;
// pull optional ban parameters from the packet
if (message.data()->getSize() == (NUM_BYTES_RFC4122_UUID + sizeof(int))) {
hasOptionalBanParameters = true;
message->readPrimitive(&banParameters);
banByUsername = banParameters & ModerationFlags::BanFlags::BAN_BY_USERNAME;
banByFingerprint = banParameters & ModerationFlags::BanFlags::BAN_BY_FINGERPRINT;
banByIP = banParameters & ModerationFlags::BanFlags::BAN_BY_IP;
}
if (!nodeUUID.isNull() && nodeUUID != sendingNode->getUUID()) {
// make sure we actually have a node with this UUID
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
@ -881,16 +918,20 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
if (!verifiedUsername.isEmpty()) {
// if we have a verified user name for this user, we first apply the kick to the username
// check if there were already permissions
bool hadPermissions = havePermissionsForName(verifiedUsername);
// if we have optional ban parameters, we should ban the username based on the parameter
if (!hasOptionalBanParameters || banByUsername) {
// check if there were already permissions
bool hadPermissions = havePermissionsForName(verifiedUsername);
// grab or create permissions for the given username
auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
// grab or create permissions for the given username
auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain);
newPermissions =
!hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain);
// ensure that the connect permission is clear
userPermissions->clear(NodePermissions::Permission::canConnectToDomain);
// ensure that the connect permission is clear
userPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
}
// if we didn't have a username, or this domain-server uses the "multi-kick" setting to
@ -898,7 +939,7 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
// then we remove connect permissions for the machine fingerprint (or IP as fallback)
const QString MULTI_KICK_SETTINGS_KEYPATH = "security.multi_kick_logged_in";
if (verifiedUsername.isEmpty() || valueOrDefaultValueForKeyPath(MULTI_KICK_SETTINGS_KEYPATH).toBool()) {
if (banByFingerprint || verifiedUsername.isEmpty() || valueOrDefaultValueForKeyPath(MULTI_KICK_SETTINGS_KEYPATH).toBool()) {
// remove connect permissions for the machine fingerprint
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
if (nodeData) {
@ -923,36 +964,39 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
fingerprintPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
} else {
// if no node data, all we can do is IP address
auto& kickAddress = matchingNode->getActiveSocket()
? matchingNode->getActiveSocket()->getAddress()
: matchingNode->getPublicSocket().getAddress();
// if no node data, all we can do is ban by IP address
banByIP = true;
}
}
if (banByIP) {
auto& kickAddress = matchingNode->getActiveSocket()
? matchingNode->getActiveSocket()->getAddress()
: matchingNode->getPublicSocket().getAddress();
// probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server
// it is always 127.0.0.1, but looking at the public and local addresses just to be sure
// TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we
// do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it.
if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() ||
kickAddress == limitedNodeList->getLocalSockAddr().getAddress() ||
kickAddress.isLoopback() ) {
qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest";
return;
}
// probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server
// it is always 127.0.0.1, but looking at the public and local addresses just to be sure
// TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we
// do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it.
if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() ||
kickAddress == limitedNodeList->getLocalSockAddr().getAddress() ||
kickAddress.isLoopback() ) {
qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest";
return;
}
NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid());
NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid());
// check if there were already permissions for the IP
bool hadIPPermissions = hasPermissionsForIP(kickAddress);
// check if there were already permissions for the IP
bool hadIPPermissions = hasPermissionsForIP(kickAddress);
// grab or create permissions for the given IP address
auto ipPermissions = _ipPermissions[ipAddressKey];
// grab or create permissions for the given IP address
auto ipPermissions = _ipPermissions[ipAddressKey];
if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
newPermissions = true;
if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
newPermissions = true;
ipPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
ipPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
}
@ -1448,6 +1492,8 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt
SettingsBackupFlag settingsBackupFlag) {
QJsonObject responseObject;
responseObject["version"] = _descriptionVersion; // Domain settings version number.
if (!typeValue.isEmpty() || authentication == Authenticated) {
// convert the string type value to a QJsonValue
QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt());

View file

@ -19,6 +19,7 @@ endif ()
include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}")
setup_memory_debugger()
setup_thread_debugger()
# append OpenSSL to our list of libraries to link
target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES})

View file

@ -47,6 +47,7 @@ add_custom_target(resources ALL DEPENDS ${GENERATE_QRC_DEPENDS})
set(OPTIONAL_EXTERNALS "LeapMotion")
setup_memory_debugger()
setup_thread_debugger()
foreach(EXTERNAL ${OPTIONAL_EXTERNALS})
string(TOUPPER ${EXTERNAL} ${EXTERNAL}_UPPERCASE)
@ -299,7 +300,7 @@ target_link_libraries(
${PLATFORM_QT_LIBRARIES}
)
if (UNIX AND NOT ANDROID)
if (UNIX AND NOT ANDROID AND NOT VIRCADIA_THREAD_DEBUGGING)
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
# Linux
target_link_libraries(${TARGET_NAME} pthread atomic)

View file

@ -518,7 +518,12 @@ Rectangle {
glyphText: "\ue02e"
onClicked: {
adjustWearables.open(currentAvatar);
if (!AddressManager.isConnected || Entities.canRezAvatarEntities()) {
adjustWearables.open(currentAvatar);
} else {
Window.alert("You cannot use wearables on this domain.")
}
}
}
@ -529,7 +534,11 @@ Rectangle {
glyphText: wearablesFrozen ? hifi.glyphs.lock : hifi.glyphs.unlock;
onClicked: {
emitSendToScript({'method' : 'toggleWearablesFrozen'});
if (!AddressManager.isConnected || Entities.canRezAvatarEntities()) {
emitSendToScript({'method' : 'toggleWearablesFrozen'});
} else {
Window.alert("You cannot use wearables on this domain.")
}
}
}
}

View file

@ -478,7 +478,7 @@ Rectangle {
visible: iAmAdmin;
role: "mute";
title: "SILENCE";
width: actionButtonWidth;
width: actionButtonWidth - 8;
movable: false;
resizable: false;
}
@ -506,6 +506,7 @@ Rectangle {
id: itemCell;
property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore";
property bool isButton: styleData.role === "mute" || styleData.role === "kick";
property bool isBan: styleData.role === "kick";
property bool isAvgAudio: styleData.role === "avgAudioLevel";
opacity: !isButton ? (model && model.isPresent ? 1.0 : 0.4) : 1.0; // Admin actions shouldn't turn gray
@ -605,7 +606,9 @@ Rectangle {
color: 2; // Red
visible: isButton;
enabled: !nameCard.isReplicated;
anchors.centerIn: parent;
anchors.verticalCenter: itemCell.verticalCenter;
anchors.left: parent.left;
anchors.leftMargin: styleData.role === "kick" ? 1 : 14;
width: 32;
height: 32;
onClicked: {
@ -620,7 +623,39 @@ Rectangle {
HiFiGlyphs {
text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted;
// Size
size: parent.height*1.3;
size: parent.height * 1.3;
// Anchors
anchors.fill: parent;
// Style
horizontalAlignment: Text.AlignHCenter;
color: enabled ? hifi.buttons.textColor[actionButton.color]
: hifi.buttons.disabledTextColor[actionButton.colorScheme];
}
}
HifiControlsUit.Button {
id: hardBanButton;
color: 2; // Red
visible: isBan;
enabled: !nameCard.isReplicated;
anchors.verticalCenter: itemCell.verticalCenter;
anchors.left: parent.left;
anchors.leftMargin: actionButton.width + 3;
width: 32;
height: 32;
onClicked: {
Users[styleData.role](model.sessionId, Users.BAN_BY_USERNAME | Users.BAN_BY_FINGERPRINT | Users.BAN_BY_IP);
UserActivityLogger["palAction"](styleData.role, model.sessionId);
if (styleData.role === "kick") {
nearbyUserModelData.splice(model.userIndex, 1);
nearbyUserModel.remove(model.userIndex); // after changing nearbyUserModelData, b/c ListModel can frob the data
}
}
// muted/error glyphs
HiFiGlyphs {
text: hifi.glyphs.alert;
// Size
size: parent.height * 1.3;
// Anchors
anchors.fill: parent;
// Style
@ -720,7 +755,8 @@ Rectangle {
onClicked: letterbox(hifi.glyphs.question,
"Admin Actions",
"<b>Silence</b> mutes a user's microphone. Silenced users can unmute themselves by clicking &quot;UNMUTE&quot; on their toolbar.<br><br>" +
"<b>Ban</b> removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page.");
"<b>Ban (left)</b> identifies a user by username (if applicable) and machine fingerprint, then removes them from this domain and prevents them from returning. Admins can un-ban users from the Server Domain Settings page.<br><br>" +
"<b>Hard Ban (right)</b> identifies a user by username (if applicable), machine fingerprint, and IP address, then removes them from this domain and prevents them from returning. Admins can un-ban users from the Server Domain Settings page.");
onEntered: adminHelpText.color = "#94132e";
onExited: adminHelpText.color = hifi.colors.redHighlight;
}

View file

@ -1306,6 +1306,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_entityServerConnectionTimer.setSingleShot(true);
connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer);
connect(&domainHandler, &DomainHandler::confirmConnectWithoutAvatarEntities,
this, &Application::confirmConnectWithoutAvatarEntities);
connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() {
if (!isServerlessMode()) {
_entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT);
@ -2493,7 +2496,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
return viewFrustum.getPosition();
});
DependencyManager::get<UsersScriptingInterface>()->setKickConfirmationOperator([this] (const QUuid& nodeID) { userKickConfirmation(nodeID); });
DependencyManager::get<UsersScriptingInterface>()->setKickConfirmationOperator([this] (const QUuid& nodeID, unsigned int banFlags) { userKickConfirmation(nodeID, banFlags); });
render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([=](const QString& url, bool htmlContent, QSharedPointer<OffscreenQmlSurface>& webSurface, bool& cachedWebSurface) {
bool isTablet = url == TabletScriptingInterface::QML;
@ -3575,7 +3578,7 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) {
_desktopRootItemCreated = true;
}
void Application::userKickConfirmation(const QUuid& nodeID) {
void Application::userKickConfirmation(const QUuid& nodeID, unsigned int banFlags) {
auto avatarHashMap = DependencyManager::get<AvatarHashMap>();
auto avatar = avatarHashMap->getAvatarBySessionID(nodeID);
@ -3600,7 +3603,7 @@ void Application::userKickConfirmation(const QUuid& nodeID) {
// ask the NodeList to kick the user with the given session ID
if (yes) {
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID);
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID, banFlags);
}
DependencyManager::get<UsersScriptingInterface>()->setWaitForKickResponse(false);
@ -9174,6 +9177,32 @@ void Application::setShowBulletConstraintLimits(bool value) {
_physicsEngine->setShowBulletConstraintLimits(value);
}
void Application::confirmConnectWithoutAvatarEntities() {
if (_confirmConnectWithoutAvatarEntitiesDialog) {
// Dialog is already displayed.
return;
}
if (!getMyAvatar()->hasAvatarEntities()) {
// No avatar entities so continue with login.
DependencyManager::get<NodeList>()->getDomainHandler().setCanConnectWithoutAvatarEntities(true);
return;
}
QString continueMessage = "Your wearables will not display on this domain. Continue?";
_confirmConnectWithoutAvatarEntitiesDialog = OffscreenUi::asyncQuestion("Continue Without Wearables", continueMessage,
QMessageBox::Yes | QMessageBox::No);
if (_confirmConnectWithoutAvatarEntitiesDialog->getDialogItem()) {
QObject::connect(_confirmConnectWithoutAvatarEntitiesDialog, &ModalDialogListener::response, this, [=](QVariant answer) {
QObject::disconnect(_confirmConnectWithoutAvatarEntitiesDialog, &ModalDialogListener::response, this, nullptr);
_confirmConnectWithoutAvatarEntitiesDialog = nullptr;
bool shouldConnect = (static_cast<QMessageBox::StandardButton>(answer.toInt()) == QMessageBox::Yes);
DependencyManager::get<NodeList>()->getDomainHandler().setCanConnectWithoutAvatarEntities(shouldConnect);
});
}
}
void Application::createLoginDialog() {
const glm::vec3 LOGIN_DIMENSIONS { 0.89f, 0.5f, 0.01f };
const auto OFFSET = glm::vec2(0.7f, -0.1f);

View file

@ -50,6 +50,8 @@
#include <shared/ConicalViewFrustum.h>
#include <shared/FileLogger.h>
#include <RunningMarker.h>
#include <ModerationFlags.h>
#include <OffscreenUi.h>
#include "avatar/MyAvatar.h"
#include "FancyCamera.h"
@ -325,6 +327,8 @@ public:
int getOtherAvatarsReplicaCount() { return DependencyManager::get<AvatarHashMap>()->getReplicaCount(); }
void setOtherAvatarsReplicaCount(int count) { DependencyManager::get<AvatarHashMap>()->setReplicaCount(count); }
void confirmConnectWithoutAvatarEntities();
bool getLoginDialogPoppedUp() const { return _loginDialogPoppedUp; }
void createLoginDialog();
void updateLoginDialogPosition();
@ -608,7 +612,7 @@ private:
void toggleTabletUI(bool shouldOpen = false) const;
bool shouldCaptureMouse() const;
void userKickConfirmation(const QUuid& nodeID);
void userKickConfirmation(const QUuid& nodeID, unsigned int banFlags = ModerationFlags::getDefaultBanFlags());
MainWindow* _window;
QElapsedTimer& _sessionRunTimer;
@ -723,6 +727,8 @@ private:
bool _loginDialogPoppedUp{ false };
bool _desktopRootItemCreated{ false };
ModalDialogListener* _confirmConnectWithoutAvatarEntitiesDialog { nullptr };
bool _developerMenuVisible{ false };
QString _previousAvatarSkeletonModel;
float _previousAvatarTargetScale;

View file

@ -35,6 +35,7 @@ void ConnectionMonitor::init() {
connect(&domainHandler, &DomainHandler::connectedToDomain, this, &ConnectionMonitor::stopTimer);
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ConnectionMonitor::stopTimer);
connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &ConnectionMonitor::stopTimer);
connect(&domainHandler, &DomainHandler::confirmConnectWithoutAvatarEntities, this, &ConnectionMonitor::stopTimer);
connect(this, &ConnectionMonitor::setRedirectErrorState, &domainHandler, &DomainHandler::setRedirectErrorState);
auto accountManager = DependencyManager::get<AccountManager>();
connect(accountManager.data(), &AccountManager::loginComplete, this, &ConnectionMonitor::startTimer);

View file

@ -556,8 +556,6 @@ Menu::Menu() {
});
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false);
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ToggleHipsFollowing, 0, false,
avatar.get(), SLOT(setToggleHips(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawBaseOfSupport, 0, false,
avatar.get(), SLOT(setEnableDebugDrawBaseOfSupport(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawDefaultPose, 0, false,

View file

@ -211,7 +211,6 @@ namespace MenuOption {
const QString ThirdPerson = "Third Person Legacy";
const QString ThreePointCalibration = "3 Point Calibration";
const QString ThrottleFPSIfNotFocus = "Throttle FPS If Not Focus"; // FIXME - this value duplicated in Basic2DWindowOpenGLDisplayPlugin.cpp
const QString ToggleHipsFollowing = "Toggle Hips Following";
const QString ToolWindow = "Tool Window";
const QString TransmitterDrive = "Transmitter Drive";
const QString TurnWithHead = "Turn using Head";

View file

@ -544,7 +544,7 @@ void AvatarManager::removeDeadAvatarEntities(const SetOfEntities& deadEntities)
QUuid entityOwnerID = entity->getOwningAvatarID();
AvatarSharedPointer avatar = getAvatarBySessionID(entityOwnerID);
if (avatar) {
avatar->clearAvatarEntity(entity->getID());
avatar->clearAvatarEntityInternal(entity->getID());
}
}
}

View file

@ -278,6 +278,9 @@ MyAvatar::MyAvatar(QThread* thread) :
// when we leave a domain we lift whatever restrictions that domain may have placed on our scale
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &MyAvatar::leaveDomain);
auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::canRezAvatarEntitiesChanged, this, &MyAvatar::handleCanRezAvatarEntitiesChanged);
_bodySensorMatrix = deriveBodyFromHMDSensor();
using namespace recording;
@ -365,12 +368,20 @@ MyAvatar::MyAvatar(QThread* thread) :
connect(&(_skeletonModel->getRig()), &Rig::onLoadFailed, this, &MyAvatar::onLoadFailed);
_characterController.setDensity(_density);
_addAvatarEntitiesToTreeTimer.setSingleShot(true);
connect(&_addAvatarEntitiesToTreeTimer, &QTimer::timeout, [this] {
addAvatarEntitiesToTree();
});
}
MyAvatar::~MyAvatar() {
_lookAtTargetAvatar.reset();
delete _scriptEngine;
_scriptEngine = nullptr;
if (_addAvatarEntitiesToTreeTimer.isActive()) {
_addAvatarEntitiesToTreeTimer.stop();
}
}
QString MyAvatar::getDominantHand() const {
@ -1393,15 +1404,9 @@ float loadSetting(Settings& settings, const QString& name, float defaultValue) {
}
void MyAvatar::setToggleHips(bool followHead) {
_follow.setToggleHipsFollowing(followHead);
}
void MyAvatar::FollowHelper::setToggleHipsFollowing(bool followHead) {
_toggleHipsFollowing = followHead;
}
bool MyAvatar::FollowHelper::getToggleHipsFollowing() const {
return _toggleHipsFollowing;
Q_UNUSED(followHead);
qCDebug(interfaceapp) << "MyAvatar.setToggleHips is deprecated; it no longer does anything; it will soon be removed from the API; "
"please update your script";
}
void MyAvatar::setEnableDebugDrawBaseOfSupport(bool isEnabled) {
@ -1533,7 +1538,23 @@ void MyAvatar::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteAr
void MyAvatar::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
// NOTE: the requiresRemovalFromTree argument is unused
AvatarData::clearAvatarEntity(entityID);
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp) << "Ignoring clearAvatarEntity() because don't have canRezAvatarEntities permission on domain";
return;
}
clearAvatarEntityInternal(entityID);
}
void MyAvatar::clearAvatarEntityInternal(const QUuid& entityID) {
AvatarData::clearAvatarEntityInternal(entityID);
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
// Don't delete potentially non-rezzed avatar entities from cache, otherwise they're removed from settings.
return;
}
_avatarEntitiesLock.withWriteLock([&] {
_cachedAvatarEntityBlobsToDelete.push_back(entityID);
});
@ -1564,6 +1585,39 @@ void MyAvatar::sanitizeAvatarEntityProperties(EntityItemProperties& properties)
properties.markAllChanged();
}
void MyAvatar::addAvatarEntitiesToTree() {
AvatarEntityMap::const_iterator constItr = _cachedAvatarEntityBlobs.begin();
while (constItr != _cachedAvatarEntityBlobs.end()) {
QUuid id = constItr.key();
_entitiesToAdd.push_back(id); // worked once: hat shown. then unshown when permissions removed but then entity was deleted somewhere along the line!
++constItr;
}
}
bool MyAvatar::hasAvatarEntities() const {
return _cachedAvatarEntityBlobs.count() > 0;
}
void MyAvatar::handleCanRezAvatarEntitiesChanged(bool canRezAvatarEntities) {
if (canRezAvatarEntities) {
// Start displaying avatar entities.
// Allow time for the avatar mixer to be updated with the user's permissions so that it doesn't discard the avatar
// entities sent. In theory, typical worst case would be Interface running on same PC as server and the timings of
// Interface and the avatar mixer sending DomainListRequest to the domain server being such that the avatar sends its
// DomainListRequest and gets its DomainList response DOMAIN_SERVER_CHECK_IN_MSECS after Interface does. Allow extra
// time in case the avatar mixer is bogged down.
_addAvatarEntitiesToTreeTimer.start(5 * DOMAIN_SERVER_CHECK_IN_MSECS); // Single-shot.
} else {
// Cancel any pending addAvatarEntitiesToTree() call.
if (_addAvatarEntitiesToTreeTimer.isActive()) {
_addAvatarEntitiesToTreeTimer.stop();
}
// Stop displaying avatar entities.
removeAvatarEntitiesFromTree();
}
}
void MyAvatar::handleChangedAvatarEntityData() {
// NOTE: this is a per-frame update
if (getID().isNull() ||
@ -1583,6 +1637,8 @@ void MyAvatar::handleChangedAvatarEntityData() {
return;
}
bool canRezAvatarEntites = DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities();
// We collect changes to AvatarEntities and then handle them all in one spot per frame: handleChangedAvatarEntityData().
// Basically this is a "transaction pattern" with an extra complication: these changes can come from two
// "directions" and the "authoritative source" of each direction is different, so we maintain two distinct sets
@ -1669,12 +1725,15 @@ void MyAvatar::handleChangedAvatarEntityData() {
continue;
}
sanitizeAvatarEntityProperties(properties);
entityTree->withWriteLock([&] {
EntityItemPointer entity = entityTree->addEntity(id, properties);
if (entity) {
packetSender->queueEditAvatarEntityMessage(entityTree, id);
}
});
if (canRezAvatarEntites) {
entityTree->withWriteLock([&] {
EntityItemPointer entity = entityTree->addEntity(id, properties);
if (entity) {
packetSender->queueEditAvatarEntityMessage(entityTree, id);
}
});
}
}
// CHANGE real entities
@ -1692,7 +1751,7 @@ void MyAvatar::handleChangedAvatarEntityData() {
skip = true;
}
});
if (!skip) {
if (!skip && canRezAvatarEntites) {
sanitizeAvatarEntityProperties(properties);
entityTree->withWriteLock([&] {
if (entityTree->updateEntity(id, properties)) {
@ -1834,6 +1893,11 @@ AvatarEntityMap MyAvatar::getAvatarEntityData() const {
return data;
}
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp) << "Ignoring getAvatarEntityData() because don't have canRezAvatarEntities permission on domain";
return data;
}
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
@ -1879,6 +1943,12 @@ void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
// avatarEntityData is expected to be a map of QByteArrays that represent EntityItemProperties objects from JavaScript,
// aka: unfortunately-formatted-binary-blobs because we store them in non-human-readable format in Settings.
//
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp) << "Ignoring setAvatarEntityData() because don't have canRezAvatarEntities permission on domain";
return;
}
if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) {
// the data is suspect
qCDebug(interfaceapp) << "discard suspect AvatarEntityData with size =" << avatarEntityData.size();
@ -1939,6 +2009,12 @@ void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
void MyAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
// NOTE: this is an invokable Script call
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp) << "Ignoring updateAvatarEntity() because don't have canRezAvatarEntities permission on domain";
return;
}
bool changed = false;
_avatarEntitiesLock.withWriteLock([&] {
auto data = QJsonDocument::fromBinaryData(entityData);
@ -2030,7 +2106,6 @@ void MyAvatar::loadData() {
allowAvatarLeaningPreferenceStrings[static_cast<uint>(AllowAvatarLeaningPreference::Default)])));
setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible));
_follow.setToggleHipsFollowing (Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing));
setEnableDebugDrawBaseOfSupport(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawBaseOfSupport));
setEnableDebugDrawDefaultPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawDefaultPose));
setEnableDebugDrawAnimPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawAnimPose));
@ -2532,7 +2607,7 @@ void MyAvatar::removeWornAvatarEntity(const EntityItemID& entityID) {
auto entity = entityTree->findEntityByID(entityID);
if (entity && isWearableEntity(entity)) {
treeRenderer->deleteEntity(entityID);
clearAvatarEntity(entityID);
clearAvatarEntityInternal(entityID);
}
}
}
@ -2565,6 +2640,13 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() {
QVariantList avatarEntitiesData;
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
if (entityTree && !DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp)
<< "Ignoring getAvatarEntitiesVariant() because don't have canRezAvatarEntities permission on domain";
return avatarEntitiesData;
}
if (entityTree) {
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
@ -2897,6 +2979,11 @@ void MyAvatar::attach(const QString& modelURL, const QString& jointName,
);
return;
}
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp) << "Ignoring attach() because don't have canRezAvatarEntities permission on domain";
return;
}
AttachmentData data;
data.modelURL = modelURL;
data.jointName = jointName;
@ -2918,6 +3005,11 @@ void MyAvatar::detachOne(const QString& modelURL, const QString& jointName) {
);
return;
}
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp) << "Ignoring detachOne() because don't have canRezAvatarEntities permission on domain";
return;
}
QUuid entityID;
if (findAvatarEntity(modelURL, jointName, entityID)) {
DependencyManager::get<EntityScriptingInterface>()->deleteEntity(entityID);
@ -2933,6 +3025,11 @@ void MyAvatar::detachAll(const QString& modelURL, const QString& jointName) {
);
return;
}
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp) << "Ignoring detachAll() because don't have canRezAvatarEntities permission on domain";
return;
}
QUuid entityID;
while (findAvatarEntity(modelURL, jointName, entityID)) {
DependencyManager::get<EntityScriptingInterface>()->deleteEntity(entityID);
@ -2946,6 +3043,11 @@ void MyAvatar::setAttachmentData(const QVector<AttachmentData>& attachmentData)
Q_ARG(const QVector<AttachmentData>&, attachmentData));
return;
}
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp) << "Ignoring setAttachmentData() because don't have canRezAvatarEntities permission on domain";
return;
}
std::vector<EntityItemProperties> newEntitiesProperties;
for (auto& data : attachmentData) {
QUuid entityID;
@ -2968,6 +3070,12 @@ void MyAvatar::setAttachmentData(const QVector<AttachmentData>& attachmentData)
QVector<AttachmentData> MyAvatar::getAttachmentData() const {
QVector<AttachmentData> attachmentData;
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp) << "Ignoring getAttachmentData() because don't have canRezAvatarEntities permission on domain";
return attachmentData;
}
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
@ -2982,6 +3090,13 @@ QVector<AttachmentData> MyAvatar::getAttachmentData() const {
QVariantList MyAvatar::getAttachmentsVariant() const {
QVariantList result;
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp)
<< "Ignoring getAttachmentsVariant() because don't have canRezAvatarEntities permission on domain";
return result;
}
for (const auto& attachment : getAttachmentData()) {
result.append(attachment.toVariant());
}
@ -2994,6 +3109,13 @@ void MyAvatar::setAttachmentsVariant(const QVariantList& variant) {
Q_ARG(const QVariantList&, variant));
return;
}
if (!DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities()) {
qCDebug(interfaceapp)
<< "Ignoring setAttachmentsVariant() because don't have canRezAvatarEntities permission on domain";
return;
}
QVector<AttachmentData> newAttachments;
newAttachments.reserve(variant.size());
for (const auto& attachmentVar : variant) {
@ -4065,7 +4187,8 @@ float MyAvatar::getGravity() {
void MyAvatar::setSessionUUID(const QUuid& sessionUUID) {
QUuid oldSessionID = getSessionUUID();
Avatar::setSessionUUID(sessionUUID);
bool sendPackets = !DependencyManager::get<NodeList>()->getSessionUUID().isNull();
bool sendPackets = !DependencyManager::get<NodeList>()->getSessionUUID().isNull()
&& DependencyManager::get<NodeList>()->getThisNodeCanRezAvatarEntities();
if (!sendPackets) {
return;
}
@ -5514,7 +5637,7 @@ void MyAvatar::setSitStandStateChange(bool stateChanged) {
}
// Determine if the user's real-world sit/stand state has changed.
float MyAvatar::getSitStandStateChange() const {
bool MyAvatar::getSitStandStateChange() const {
return _sitStandStateChange;
}
@ -5696,7 +5819,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal_userSitting(const MyAvatar
bool stepDetected = false;
if (forwardLeanAmount > MAX_FORWARD_LEAN) {
stepDetected = true;
} else if (forwardLeanAmount < 0 && forwardLeanAmount < -MAX_BACKWARD_LEAN) {
} else if (forwardLeanAmount < -MAX_BACKWARD_LEAN) {
stepDetected = true;
} else {
stepDetected = fabs(lateralLeanAmount) > MAX_LATERAL_LEAN;

View file

@ -1454,6 +1454,7 @@ public:
void removeWornAvatarEntity(const EntityItemID& entityID);
void clearWornAvatarEntities();
bool hasAvatarEntities() const;
/**jsdoc
* Checks whether your avatar is flying.
@ -1800,7 +1801,7 @@ public:
void setAnalogPlusSprintSpeed(float value);
float getAnalogPlusSprintSpeed() const;
void setSitStandStateChange(bool stateChanged);
float getSitStandStateChange() const;
bool getSitStandStateChange() const;
void updateSitStandState(float newHeightReading, float dt);
QVector<QString> getScriptUrls();
@ -1939,6 +1940,8 @@ public:
void avatarEntityDataToJson(QJsonObject& root) const override;
void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override;
/**jsdoc
* @comment Uses the base class's JSDoc.
*/
@ -2277,12 +2280,6 @@ public slots:
*/
bool getEnableMeshVisible() const override;
/**jsdoc
* @function MyAvatar.storeAvatarEntityDataPayload
* @deprecated This function is deprecated and will be removed.
*/
void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override;
/**jsdoc
* @comment Uses the base class's JSDoc.
*/
@ -2656,6 +2653,7 @@ private slots:
protected:
void handleChangedAvatarEntityData();
void handleCanRezAvatarEntitiesChanged(bool canRezAvatarEntities);
virtual void beParentOfChild(SpatiallyNestablePointer newChild) const override;
virtual void forgetChild(SpatiallyNestablePointer newChild) const override;
virtual void recalculateChildCauterization() const override;
@ -2710,6 +2708,10 @@ private:
void attachmentDataToEntityProperties(const AttachmentData& data, EntityItemProperties& properties);
AttachmentData entityPropertiesToAttachmentData(const EntityItemProperties& properties) const;
bool findAvatarEntity(const QString& modelURL, const QString& jointName, QUuid& entityID);
void addAvatarEntitiesToTree();
// FIXME: Rename to clearAvatarEntity() once the API call is removed.
void clearAvatarEntityInternal(const QUuid& entityID) override;
bool cameraInsideHead(const glm::vec3& cameraPosition) const;
@ -2907,8 +2909,6 @@ private:
void setForceActivateVertical(bool val);
bool getForceActivateHorizontal() const;
void setForceActivateHorizontal(bool val);
bool getToggleHipsFollowing() const;
void setToggleHipsFollowing(bool followHead);
std::atomic<bool> _forceActivateRotation { false };
std::atomic<bool> _forceActivateVertical { false };
std::atomic<bool> _forceActivateHorizontal { false };
@ -3109,6 +3109,8 @@ private:
glm::vec3 _cameraEyesOffset;
float _landingAfterJumpTime { 0.0f };
QTimer _addAvatarEntitiesToTreeTimer;
};
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);

View file

@ -364,7 +364,7 @@ void setupPreferences() {
auto preference = new SpinnerSliderPreference(VR_MOVEMENT, "Camera Sensitivity", getter, setter);
preference->setMin(0.01f);
preference->setMax(5.0f);
preference->setStep(0.1);
preference->setStep(0.1f);
preference->setDecimals(2);
preferences->addPreference(preference);
}

View file

@ -91,8 +91,8 @@ static const float azimuthTable[NAZIMUTH][3] = {
// A first-order shelving filter is used to minimize the disturbance in ITD.
//
// Loosely based on data from S. Spagnol, "Distance rendering and perception of nearby virtual sound sources
// with a near-field filter model, Applied Acoustics (2017)
//
// with a near-field filter model," Applied Acoustics (2017)
//
static const int NNEARFIELD = 9;
static const float nearFieldTable[NNEARFIELD][3] = { // { b0, b1, a1 }
{ 0.008410604f, -0.000262748f, -0.991852144f }, // gain = 1/256
@ -388,7 +388,12 @@ void crossfade_4x2_AVX2(float* src, float* dst, const float* win, int numFrames)
void interpolate_AVX2(const float* src0, const float* src1, float* dst, float frac, float gain);
static void FIR_1x4(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames) {
#ifndef STACK_PROTECTOR
// Enabling -fstack-protector on gcc causes an undefined reference to FIR_1x4_AVX512 here
static auto f = cpuSupportsAVX512() ? FIR_1x4_AVX512 : (cpuSupportsAVX2() ? FIR_1x4_AVX2 : FIR_1x4_SSE);
#else
static auto f = cpuSupportsAVX2() ? FIR_1x4_AVX2 : FIR_1x4_SSE;
#endif
(*f)(src, dst0, dst1, dst2, dst3, coef, numFrames); // dispatch
}

View file

@ -1444,9 +1444,7 @@ int Avatar::getJointIndex(const QString& name) const {
}
withValidJointIndicesCache([&]() {
if (_modelJointIndicesCache.contains(name)) {
result = _modelJointIndicesCache.value(name) - 1;
}
result = _modelJointIndicesCache.value(name, result + 1) - 1;
});
return result;
}

View file

@ -1938,6 +1938,10 @@ void AvatarData::clearJointsData() {
}
int AvatarData::getFauxJointIndex(const QString& name) const {
static constexpr QChar fauxJointFirstChar('_');// The first character of all the faux joint names.
if (!name.startsWith(fauxJointFirstChar)) {
return -1;
};
if (name == "_SENSOR_TO_WORLD_MATRIX") {
return SENSOR_TO_WORLD_MATRIX_INDEX;
}
@ -2246,7 +2250,7 @@ void AvatarData::processTraitInstance(AvatarTraits::TraitType traitType,
void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID) {
if (traitType == AvatarTraits::AvatarEntity) {
clearAvatarEntity(instanceID);
clearAvatarEntityInternal(instanceID);
} else if (traitType == AvatarTraits::Grab) {
clearAvatarGrabData(instanceID);
}
@ -3034,6 +3038,10 @@ void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& ent
void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
// NOTE: requiresRemovalFromTree is unused
clearAvatarEntityInternal(entityID);
}
void AvatarData::clearAvatarEntityInternal(const QUuid& entityID) {
bool removedEntity = false;
_avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] {
removedEntity = _packedAvatarEntityData.remove(entityID);
@ -3046,6 +3054,24 @@ void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFr
}
}
void AvatarData::clearAvatarEntities() {
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
for (const auto& entityID : avatarEntityIDs) {
clearAvatarEntityInternal(entityID);
}
}
QList<QUuid> AvatarData::getAvatarEntityIDs() const {
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
return avatarEntityIDs;
}
AvatarEntityMap AvatarData::getAvatarEntityData() const {
// overridden where needed
// NOTE: the return value is expected to be a map of unfortunately-formatted-binary-blobs

View file

@ -1186,6 +1186,13 @@ public:
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true);
// FIXME: Rename to clearAvatarEntity() once the API call is removed.
virtual void clearAvatarEntityInternal(const QUuid& entityID);
void clearAvatarEntities();
QList<QUuid> getAvatarEntityIDs() const;
/**jsdoc
* Enables blend shapes set using {@link Avatar.setBlendshape} or {@link MyAvatar.setBlendshape} to be transmitted to other

View file

@ -168,20 +168,20 @@ void TextureBaker::processTexture() {
gpu::BackendTarget::GLES32
}};
for (auto target : BACKEND_TARGETS) {
auto processedTexture = image::processImage(buffer, _textureURL.toString().toStdString(), image::ColorChannel::NONE,
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, true,
target, _abortProcessing);
if (!processedTexture) {
auto processedTextureAndSize = image::processImage(buffer, _textureURL.toString().toStdString(), image::ColorChannel::NONE,
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, true,
target, _abortProcessing);
if (!processedTextureAndSize.first) {
handleError("Could not process texture " + _textureURL.toString());
return;
}
processedTexture->setSourceHash(hash);
processedTextureAndSize.first->setSourceHash(hash);
if (shouldStop()) {
return;
}
auto memKTX = gpu::Texture::serialize(*processedTexture);
auto memKTX = gpu::Texture::serialize(*processedTextureAndSize.first, processedTextureAndSize.second);
if (!memKTX) {
handleError("Could not serialize " + _textureURL.toString() + " to KTX");
return;
@ -211,19 +211,19 @@ void TextureBaker::processTexture() {
// Uncompressed KTX
if (_textureType == image::TextureUsage::Type::SKY_TEXTURE || _textureType == image::TextureUsage::Type::AMBIENT_TEXTURE) {
buffer->reset();
auto processedTexture = image::processImage(std::move(buffer), _textureURL.toString().toStdString(), image::ColorChannel::NONE,
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, false, gpu::BackendTarget::GL45, _abortProcessing);
if (!processedTexture) {
auto processedTextureAndSize = image::processImage(std::move(buffer), _textureURL.toString().toStdString(), image::ColorChannel::NONE,
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, false, gpu::BackendTarget::GL45, _abortProcessing);
if (!processedTextureAndSize.first) {
handleError("Could not process texture " + _textureURL.toString());
return;
}
processedTexture->setSourceHash(hash);
processedTextureAndSize.first->setSourceHash(hash);
if (shouldStop()) {
return;
}
auto memKTX = gpu::Texture::serialize(*processedTexture);
auto memKTX = gpu::Texture::serialize(*processedTextureAndSize.first, processedTextureAndSize.second);
if (!memKTX) {
handleError("Could not serialize " + _textureURL.toString() + " to KTX");
return;

View file

@ -78,6 +78,9 @@ public:
/// Returns a pointer to the underlying socket, to which WebSocket message bodies should be written.
QTcpSocket* socket() const { return _socket; }
/// Returns the IP address on the other side of the connection
const QHostAddress &peerAddress() const { return _address; }
/// Returns the request operation.
QNetworkAccessManager::Operation requestOperation() const { return _requestOperation; }

View file

@ -29,6 +29,8 @@
#include "RenderableZoneEntityItem.h"
#include "RenderableMaterialEntityItem.h"
#include "RenderPipelines.h"
using namespace render;
using namespace render::entities;
@ -149,10 +151,11 @@ Item::Bound EntityRenderer::getBound(RenderArgs* args) {
}
ShapeKey EntityRenderer::getShapeKey() {
ShapeKey::Builder builder = ShapeKey::Builder().withOwnPipeline();
if (_primitiveMode == PrimitiveMode::LINES) {
return ShapeKey::Builder().withOwnPipeline().withWireframe();
builder.withWireframe();
}
return ShapeKey::Builder().withOwnPipeline();
return builder.build();
}
render::hifi::Tag EntityRenderer::getTagMask() const {
@ -365,6 +368,7 @@ bool EntityRenderer::needsRenderUpdate() const {
if (_prevIsTransparent != isTransparent()) {
return true;
}
return needsRenderUpdateFromEntity(_entity);
}
@ -491,6 +495,176 @@ void EntityRenderer::removeMaterial(graphics::MaterialPointer material, const st
emit requestRenderUpdate();
}
EntityRenderer::Pipeline EntityRenderer::getPipelineType(const graphics::MultiMaterial& materials) {
if (materials.top().material && materials.top().material->isProcedural() && materials.top().material->isReady()) {
return Pipeline::PROCEDURAL;
}
graphics::MaterialKey drawMaterialKey = materials.getMaterialKey();
if (drawMaterialKey.isEmissive() || drawMaterialKey.isMetallic() || drawMaterialKey.isScattering()) {
return Pipeline::MATERIAL;
}
// If the material is using any map, we need to use a material ShapeKey
for (int i = 0; i < graphics::Material::MapChannel::NUM_MAP_CHANNELS; i++) {
if (drawMaterialKey.isMapChannel(graphics::Material::MapChannel(i))) {
return Pipeline::MATERIAL;
}
}
return Pipeline::SIMPLE;
}
bool EntityRenderer::needsRenderUpdateFromMaterials() const {
MaterialMap::const_iterator materials;
{
std::lock_guard<std::mutex> lock(_materialsLock);
materials = _materials.find("0");
if (materials == _materials.cend()) {
return false;
}
}
if (materials->second.shouldUpdate()) {
return true;
}
if (materials->second.top().material && materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(materials->second.top().material);
if (procedural->isFading()) {
return true;
}
}
return false;
}
void EntityRenderer::updateMaterials(bool baseMaterialChanged) {
MaterialMap::iterator materials;
{
std::lock_guard<std::mutex> lock(_materialsLock);
materials = _materials.find("0");
if (materials == _materials.end()) {
return;
}
}
if (baseMaterialChanged) {
materials->second.setNeedsUpdate(true);
}
bool requestUpdate = false;
if (materials->second.top().material && materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(materials->second.top().material);
if (procedural->isFading()) {
procedural->setIsFading(Interpolate::calculateFadeRatio(procedural->getFadeStartTime()) < 1.0f);
requestUpdate = true;
}
}
if (materials->second.shouldUpdate()) {
RenderPipelines::updateMultiMaterial(materials->second);
requestUpdate = true;
}
if (requestUpdate) {
emit requestRenderUpdate();
}
}
bool EntityRenderer::materialsTransparent() const {
MaterialMap::const_iterator materials;
{
std::lock_guard<std::mutex> lock(_materialsLock);
materials = _materials.find("0");
if (materials == _materials.cend()) {
return false;
}
}
if (materials->second.top().material) {
if (materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(materials->second.top().material);
if (procedural->isFading()) {
return true;
}
}
if (materials->second.getMaterialKey().isTranslucent()) {
return true;
}
}
return false;
}
Item::Bound EntityRenderer::getMaterialBound(RenderArgs* args) {
MaterialMap::iterator materials;
{
std::lock_guard<std::mutex> lock(_materialsLock);
materials = _materials.find("0");
if (materials == _materials.end()) {
return EntityRenderer::getBound(args);
}
}
if (materials->second.top().material && materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(materials->second.top().material);
if (procedural->hasVertexShader() && procedural->hasBoundOperator()) {
return procedural->getBound(args);
}
}
return EntityRenderer::getBound(args);
}
void EntityRenderer::updateShapeKeyBuilderFromMaterials(ShapeKey::Builder& builder) {
MaterialMap::iterator materials;
{
std::lock_guard<std::mutex> lock(_materialsLock);
materials = _materials.find("0");
if (materials != _materials.end()) {
if (materials->second.shouldUpdate()) {
RenderPipelines::updateMultiMaterial(materials->second);
}
} else {
return;
}
}
if (isTransparent()) {
builder.withTranslucent();
}
if (_primitiveMode == PrimitiveMode::LINES) {
builder.withWireframe();
}
builder.withCullFaceMode(materials->second.getCullFaceMode());
graphics::MaterialKey drawMaterialKey = materials->second.getMaterialKey();
if (drawMaterialKey.isUnlit()) {
builder.withUnlit();
}
auto pipelineType = getPipelineType(materials->second);
if (pipelineType == Pipeline::MATERIAL) {
builder.withMaterial();
if (drawMaterialKey.isNormalMap()) {
builder.withTangents();
}
if (drawMaterialKey.isLightMap()) {
builder.withLightMap();
}
} else if (pipelineType == Pipeline::PROCEDURAL) {
builder.withOwnPipeline();
}
}
glm::vec4 EntityRenderer::calculatePulseColor(const glm::vec4& color, const PulsePropertyGroup& pulseProperties, quint64 start) {
if (pulseProperties.getPeriod() == 0.0f || (pulseProperties.getColorMode() == PulseMode::NONE && pulseProperties.getAlphaMode() == PulseMode::NONE)) {
return color;

View file

@ -55,8 +55,14 @@ public:
const uint64_t& getUpdateTime() const { return _updateTime; }
enum class Pipeline {
SIMPLE,
MATERIAL,
PROCEDURAL
};
virtual void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName);
virtual void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName);
static Pipeline getPipelineType(const graphics::MultiMaterial& materials);
virtual scriptable::ScriptableModelBase getScriptableModel() override { return scriptable::ScriptableModelBase(); }
@ -117,6 +123,14 @@ protected:
Transform getTransformToCenterWithMaybeOnlyLocalRotation(const EntityItemPointer& entity, bool& success) const;
// Shared methods for entities that support materials
using MaterialMap = std::unordered_map<std::string, graphics::MultiMaterial>;
bool needsRenderUpdateFromMaterials() const;
void updateMaterials(bool baseMaterialChanged = false);
bool materialsTransparent() const;
Item::Bound getMaterialBound(RenderArgs* args);
void updateShapeKeyBuilderFromMaterials(ShapeKey::Builder& builder);
Item::Bound _bound;
SharedSoundPointer _collisionSound;
QUuid _changeHandlerId;
@ -132,13 +146,13 @@ protected:
RenderLayer _renderLayer { RenderLayer::WORLD };
PrimitiveMode _primitiveMode { PrimitiveMode::SOLID };
QVector<QUuid> _renderWithZones;
BillboardMode _billboardMode;
BillboardMode _billboardMode { BillboardMode::NONE };
bool _cauterized { false };
bool _moving { false };
Transform _renderTransform;
std::unordered_map<std::string, graphics::MultiMaterial> _materials;
std::mutex _materialsLock;
MaterialMap _materials;
mutable std::mutex _materialsLock;
quint64 _created;

View file

@ -11,10 +11,15 @@
#include <DependencyManager.h>
#include <GeometryCache.h>
#include "RenderPipelines.h"
using namespace render;
using namespace render::entities;
GizmoEntityRenderer::GizmoEntityRenderer(const EntityItemPointer& entity) : Parent(entity) {}
GizmoEntityRenderer::GizmoEntityRenderer(const EntityItemPointer& entity) : Parent(entity) {
_material->setCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE);
addMaterial(graphics::MaterialLayer(_material, 0), "0");
}
GizmoEntityRenderer::~GizmoEntityRenderer() {
auto geometryCache = DependencyManager::get<GeometryCache>();
@ -31,12 +36,8 @@ GizmoEntityRenderer::~GizmoEntityRenderer() {
}
}
bool GizmoEntityRenderer::isTransparent() const {
bool ringTransparent = _gizmoType == GizmoType::RING && (_ringProperties.getInnerStartAlpha() < 1.0f ||
_ringProperties.getInnerEndAlpha() < 1.0f || _ringProperties.getOuterStartAlpha() < 1.0f ||
_ringProperties.getOuterEndAlpha() < 1.0f);
return Parent::isTransparent() || ringTransparent;
bool GizmoEntityRenderer::needsRenderUpdate() const {
return needsRenderUpdateFromMaterials() || Parent::needsRenderUpdate();
}
void GizmoEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
@ -193,10 +194,20 @@ void GizmoEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPoint
}
}
}
updateMaterials();
}
bool GizmoEntityRenderer::isTransparent() const {
bool ringTransparent = _gizmoType == GizmoType::RING && (_ringProperties.getInnerStartAlpha() < 1.0f ||
_ringProperties.getInnerEndAlpha() < 1.0f || _ringProperties.getOuterStartAlpha() < 1.0f ||
_ringProperties.getOuterEndAlpha() < 1.0f);
return ringTransparent || Parent::isTransparent() || materialsTransparent();
}
Item::Bound GizmoEntityRenderer::getBound(RenderArgs* args) {
auto bound = Parent::getBound(args);
auto bound = Parent::getMaterialBound(args);
if (_ringProperties.getHasTickMarks()) {
glm::vec3 scale = bound.getScale();
for (int i = 0; i < 3; i += 2) {
@ -220,13 +231,8 @@ Item::Bound GizmoEntityRenderer::getBound(RenderArgs* args) {
}
ShapeKey GizmoEntityRenderer::getShapeKey() {
auto builder = render::ShapeKey::Builder().withoutCullFace();
if (isTransparent()) {
builder.withTranslucent();
}
if (_primitiveMode == PrimitiveMode::LINES) {
builder.withUnlit().withDepthBias();
}
auto builder = render::ShapeKey::Builder().withDepthBias();
updateShapeKeyBuilderFromMaterials(builder);
return builder.build();
}
@ -249,15 +255,29 @@ void GizmoEntityRenderer::doRender(RenderArgs* args) {
transparent = isTransparent();
});
bool wireframe = render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES;
bool forward = _renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD;
graphics::MultiMaterial materials;
{
std::lock_guard<std::mutex> lock(_materialsLock);
materials = _materials["0"];
}
geometryCache->bindSimpleProgram(batch, false, transparent, wireframe, true, true, forward, graphics::MaterialKey::CULL_NONE);
bool wireframe = render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES;
transform.setRotation(BillboardModeHelpers::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode,
args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE ? BillboardModeHelpers::getPrimaryViewFrustumPosition() : args->getViewFrustum().getPosition(), true));
batch.setModelTransform(transform);
Pipeline pipelineType = getPipelineType(materials);
if (pipelineType == Pipeline::PROCEDURAL) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(materials.top().material);
transparent |= procedural->isFading();
procedural->prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(transparent));
} else if (pipelineType == Pipeline::MATERIAL) {
if (RenderPipelines::bindMaterials(materials, batch, args->_renderMode, args->_enableTexturing)) {
args->_details._materialSwitches++;
}
}
// Background circle
geometryCache->renderVertices(batch, wireframe ? gpu::LINE_STRIP : _solidPrimitive, _ringGeometryID);

View file

@ -13,6 +13,8 @@
#include <GizmoEntityItem.h>
#include <procedural/Procedural.h>
namespace render { namespace entities {
class GizmoEntityRenderer : public TypedEntityRenderer<GizmoEntityItem> {
@ -29,10 +31,12 @@ protected:
bool isTransparent() const override;
private:
virtual bool needsRenderUpdate() const override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
virtual void doRender(RenderArgs* args) override;
std::shared_ptr<graphics::ProceduralMaterial> _material { std::make_shared<graphics::ProceduralMaterial>() };
GizmoType _gizmoType { UNSET_GIZMO_TYPE };
RingGizmoPropertyGroup _ringProperties;
PrimitiveMode _prevPrimitiveMode;

View file

@ -10,12 +10,17 @@
#include <DependencyManager.h>
#include <GeometryCache.h>
#include <graphics/ShaderConstants.h>
#include "RenderPipelines.h"
using namespace render;
using namespace render::entities;
ImageEntityRenderer::ImageEntityRenderer(const EntityItemPointer& entity) : Parent(entity) {
_geometryId = DependencyManager::get<GeometryCache>()->allocateID();
_material->setCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE);
addMaterial(graphics::MaterialLayer(_material, 0), "0");
}
ImageEntityRenderer::~ImageEntityRenderer() {
@ -25,8 +30,8 @@ ImageEntityRenderer::~ImageEntityRenderer() {
}
}
bool ImageEntityRenderer::isTransparent() const {
return Parent::isTransparent() || (_textureIsLoaded && _texture->getGPUTexture() && _texture->getGPUTexture()->getUsage().isAlpha()) || _alpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE;
bool ImageEntityRenderer::needsRenderUpdate() const {
return needsRenderUpdateFromMaterials() || Parent::needsRenderUpdate();
}
void ImageEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
@ -51,67 +56,111 @@ void ImageEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPoint
_textureIsLoaded = false;
}
_emissive = entity->getEmissive();
_keepAspectRatio = entity->getKeepAspectRatio();
_subImage = entity->getSubImage();
_color = entity->getColor();
_alpha = entity->getAlpha();
_pulseProperties = entity->getPulseProperties();
bool materialChanged = false;
glm::vec3 color = toGlm(entity->getColor());
if (_color != color) {
_color = color;
_material->setAlbedo(color);
materialChanged = true;
}
float alpha = entity->getAlpha();
if (_alpha != alpha) {
_alpha = alpha;
_material->setOpacity(alpha);
materialChanged = true;
}
auto emissive = entity->getEmissive();
if (_emissive != emissive) {
_emissive = emissive;
_material->setUnlit(_emissive);
materialChanged = true;
}
updateMaterials(materialChanged);
bool nextTextureLoaded = _texture && (_texture->isLoaded() || _texture->isFailed());
if (!_textureIsLoaded) {
emit requestRenderUpdate();
if (nextTextureLoaded) {
float width = _texture->getOriginalWidth();
float height = _texture->getOriginalHeight();
glm::vec3 naturalDimensions = glm::vec3(1.0f, 1.0f, 0.01f);
if (width < height) {
naturalDimensions.x = width / height;
} else {
naturalDimensions.y = height / width;
}
// Unlike Models (where the Renderer also doubles as the EntityItem), Images need to
// convey this information back to the game object from the Renderer
entity->setNaturalDimension(naturalDimensions);
}
}
_textureIsLoaded = _texture && (_texture->isLoaded() || _texture->isFailed());
_textureIsLoaded = nextTextureLoaded;
}
bool ImageEntityRenderer::isTransparent() const {
bool imageTransparent = _alpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE ||
(_textureIsLoaded && _texture->getGPUTexture() && _texture->getGPUTexture()->getUsage().isAlpha());
return imageTransparent || Parent::isTransparent() || materialsTransparent();
}
Item::Bound ImageEntityRenderer::getBound(RenderArgs* args) {
return Parent::getMaterialBound(args);
}
ShapeKey ImageEntityRenderer::getShapeKey() {
auto builder = render::ShapeKey::Builder().withoutCullFace().withDepthBias();
if (isTransparent()) {
builder.withTranslucent();
}
if (_emissive) {
builder.withUnlit();
}
if (_primitiveMode == PrimitiveMode::LINES) {
builder.withWireframe();
}
auto builder = render::ShapeKey::Builder().withDepthBias();
updateShapeKeyBuilderFromMaterials(builder);
return builder.build();
}
void ImageEntityRenderer::doRender(RenderArgs* args) {
glm::vec4 color = glm::vec4(toGlm(_color), _alpha);
color = EntityRenderer::calculatePulseColor(color, _pulseProperties, _created);
Transform transform;
withReadLock([&] {
transform = _renderTransform;
});
PerformanceTimer perfTimer("RenderableImageEntityItem::render");
Q_ASSERT(args->_batch);
if (!_visible || !_texture || !_texture->isLoaded() || color.a == 0.0f) {
graphics::MultiMaterial materials;
{
std::lock_guard<std::mutex> lock(_materialsLock);
materials = _materials["0"];
}
auto& schema = materials.getSchemaBuffer().get<graphics::MultiMaterial::Schema>();
glm::vec4 color = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity);
color = EntityRenderer::calculatePulseColor(color, _pulseProperties, _created);
if (!_texture || !_texture->isLoaded() || color.a == 0.0f) {
return;
}
Q_ASSERT(args->_batch);
Transform transform;
bool transparent;
withReadLock([&] {
transform = _renderTransform;
transparent = isTransparent();
});
gpu::Batch* batch = args->_batch;
transform.setRotation(BillboardModeHelpers::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode,
args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE ? BillboardModeHelpers::getPrimaryViewFrustumPosition() : args->getViewFrustum().getPosition()));
batch->setModelTransform(transform);
batch->setResourceTexture(0, _texture->getGPUTexture());
float imageWidth = _texture->getWidth();
float imageHeight = _texture->getHeight();
float originalWidth = _texture->getOriginalWidth();
float originalHeight = _texture->getOriginalHeight();
QRect fromImage;
if (_subImage.width() <= 0) {
fromImage.setX(0);
fromImage.setWidth(imageWidth);
} else {
float scaleX = imageWidth / _texture->getOriginalWidth();
float scaleX = imageWidth / originalWidth;
fromImage.setX(scaleX * _subImage.x());
fromImage.setWidth(scaleX * _subImage.width());
}
@ -120,22 +169,48 @@ void ImageEntityRenderer::doRender(RenderArgs* args) {
fromImage.setY(0);
fromImage.setHeight(imageHeight);
} else {
float scaleY = imageHeight / _texture->getOriginalHeight();
float scaleY = imageHeight / originalHeight;
fromImage.setY(scaleY * _subImage.y());
fromImage.setHeight(scaleY * _subImage.height());
}
float maxSize = glm::max(fromImage.width(), fromImage.height());
float x = _keepAspectRatio ? fromImage.width() / (2.0f * maxSize) : 0.5f;
float y = _keepAspectRatio ? fromImage.height() / (2.0f * maxSize) : 0.5f;
glm::vec2 texCoordBottomLeft((fromImage.x() + 0.5f) / imageWidth, (fromImage.y() + fromImage.height() - 0.5f) / imageHeight);
glm::vec2 texCoordTopRight((fromImage.x() + fromImage.width() - 0.5f) / imageWidth, (fromImage.y() + 0.5f) / imageHeight);
if (_keepAspectRatio) {
glm::vec3 scale = transform.getScale();
float targetAspectRatio = originalWidth / originalHeight;
float currentAspectRatio = scale.x / scale.y;
if (targetAspectRatio < currentAspectRatio) {
scale.x *= targetAspectRatio / currentAspectRatio;
} else {
scale.y /= targetAspectRatio / currentAspectRatio;
}
transform.setScale(scale);
}
batch->setModelTransform(transform);
Pipeline pipelineType = getPipelineType(materials);
if (pipelineType == Pipeline::PROCEDURAL) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(materials.top().material);
transparent |= procedural->isFading();
procedural->prepare(*batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(transparent));
} else if (pipelineType == Pipeline::SIMPLE) {
batch->setResourceTexture(0, _texture->getGPUTexture());
} else {
if (RenderPipelines::bindMaterials(materials, *batch, args->_renderMode, args->_enableTexturing)) {
args->_details._materialSwitches++;
}
}
DependencyManager::get<GeometryCache>()->renderQuad(
*batch, glm::vec2(-x, -y), glm::vec2(x, y), texCoordBottomLeft, texCoordTopRight,
*batch, glm::vec2(-0.5f), glm::vec2(0.5f), texCoordBottomLeft, texCoordTopRight,
color, _geometryId
);
batch->setResourceTexture(0, nullptr);
if (pipelineType == Pipeline::SIMPLE) {
// we have to reset this to white for other simple shapes
batch->setResourceTexture(graphics::slot::texture::Texture::MaterialAlbedo, DependencyManager::get<TextureCache>()->getWhiteTexture());
}
}

View file

@ -13,6 +13,8 @@
#include <ImageEntityItem.h>
#include <procedural/Procedural.h>
namespace render { namespace entities {
class ImageEntityRenderer : public TypedEntityRenderer<ImageEntityItem> {
@ -23,11 +25,13 @@ public:
~ImageEntityRenderer();
protected:
Item::Bound getBound(RenderArgs* args) override;
ShapeKey getShapeKey() override;
bool isTransparent() const override;
private:
virtual bool needsRenderUpdate() const override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
virtual void doRender(RenderArgs* args) override;
@ -40,8 +44,9 @@ private:
bool _keepAspectRatio;
QRect _subImage;
glm::u8vec3 _color;
float _alpha;
std::shared_ptr<graphics::ProceduralMaterial> _material { std::make_shared<graphics::ProceduralMaterial>() };
glm::vec3 _color { NAN };
float _alpha { NAN };
PulsePropertyGroup _pulseProperties;
int _geometryId { 0 };

View file

@ -219,6 +219,10 @@ ShapeKey MaterialEntityRenderer::getShapeKey() {
builder.withTranslucent();
}
if (drawMaterial) {
builder.withCullFaceMode(drawMaterial->getCullFaceMode());
}
if (drawMaterial && drawMaterial->isProcedural() && drawMaterial->isReady()) {
builder.withOwnPipeline();
} else {

View file

@ -1254,13 +1254,13 @@ void ModelEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPoint
if (_hasModel && !model) {
model = std::make_shared<Model>(nullptr, entity.get(), _created);
connect(model.get(), &Model::requestRenderUpdate, this, &ModelEntityRenderer::requestRenderUpdate);
connect(model.get(), &Model::setURLFinished, this, [&](bool didVisualGeometryRequestSucceed) {
connect(model.get(), &Model::setURLFinished, this, [=](bool didVisualGeometryRequestSucceed) {
_didLastVisualGeometryRequestSucceed = didVisualGeometryRequestSucceed;
const render::ScenePointer& scene = AbstractViewStateInterface::instance()->getMain3DScene();
render::Transaction transaction;
transaction.updateItem<PayloadProxyInterface>(_renderItemID, [&](PayloadProxyInterface& self) {
transaction.updateItem<PayloadProxyInterface>(_renderItemID, [=](PayloadProxyInterface& self) {
const render::ScenePointer& scene = AbstractViewStateInterface::instance()->getMain3DScene();
withWriteLock([&] {
withWriteLock([=] {
setKey(didVisualGeometryRequestSucceed, _model);
_model->setVisibleInScene(_visible, scene);
_model->setCauterized(_cauterized, scene);

View file

@ -29,26 +29,7 @@ ShapeEntityRenderer::ShapeEntityRenderer(const EntityItemPointer& entity) : Pare
}
bool ShapeEntityRenderer::needsRenderUpdate() const {
if (resultWithReadLock<bool>([&] {
auto mat = _materials.find("0");
if (mat != _materials.end() && mat->second.top().material && mat->second.top().material->isProcedural() &&
mat->second.top().material->isReady()) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(mat->second.top().material);
if (procedural->isFading()) {
return true;
}
}
if (mat != _materials.end() && mat->second.shouldUpdate()) {
return true;
}
return false;
})) {
return true;
}
return Parent::needsRenderUpdate();
return needsRenderUpdateFromMaterials() || Parent::needsRenderUpdate();
}
void ShapeEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
@ -91,155 +72,58 @@ void ShapeEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPoint
materialChanged = true;
}
withReadLock([&] {
auto materials = _materials.find("0");
if (materials != _materials.end()) {
if (materialChanged) {
materials->second.setNeedsUpdate(true);
}
bool requestUpdate = false;
if (materials->second.top().material && materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(materials->second.top().material);
if (procedural->isFading()) {
procedural->setIsFading(Interpolate::calculateFadeRatio(procedural->getFadeStartTime()) < 1.0f);
requestUpdate = true;
}
}
if (materials->second.shouldUpdate()) {
RenderPipelines::updateMultiMaterial(materials->second);
requestUpdate = true;
}
if (requestUpdate) {
emit requestRenderUpdate();
}
}
});
updateMaterials(materialChanged);
}
bool ShapeEntityRenderer::isTransparent() const {
if (_pulseProperties.getAlphaMode() != PulseMode::NONE) {
return true;
}
auto mat = _materials.find("0");
if (mat != _materials.end() && mat->second.top().material) {
if (mat->second.top().material->isProcedural() && mat->second.top().material->isReady()) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(mat->second.top().material);
if (procedural->isFading()) {
return true;
}
}
if (mat->second.getMaterialKey().isTranslucent()) {
return true;
}
}
return Parent::isTransparent();
return _pulseProperties.getAlphaMode() != PulseMode::NONE || Parent::isTransparent() || materialsTransparent();
}
ShapeEntityRenderer::Pipeline ShapeEntityRenderer::getPipelineType(const graphics::MultiMaterial& materials) const {
if (materials.top().material && materials.top().material->isProcedural() && materials.top().material->isReady()) {
return Pipeline::PROCEDURAL;
}
graphics::MaterialKey drawMaterialKey = materials.getMaterialKey();
if (drawMaterialKey.isEmissive() || drawMaterialKey.isUnlit() || drawMaterialKey.isMetallic() || drawMaterialKey.isScattering()) {
return Pipeline::MATERIAL;
}
// If the material is using any map, we need to use a material ShapeKey
for (int i = 0; i < graphics::Material::MapChannel::NUM_MAP_CHANNELS; i++) {
if (drawMaterialKey.isMapChannel(graphics::Material::MapChannel(i))) {
return Pipeline::MATERIAL;
}
}
return Pipeline::SIMPLE;
Item::Bound ShapeEntityRenderer::getBound(RenderArgs* args) {
return Parent::getMaterialBound(args);
}
ShapeKey ShapeEntityRenderer::getShapeKey() {
ShapeKey::Builder builder;
auto mat = _materials.find("0");
if (mat != _materials.end() && mat->second.shouldUpdate()) {
RenderPipelines::updateMultiMaterial(mat->second);
}
if (isTransparent()) {
builder.withTranslucent();
}
if (_primitiveMode == PrimitiveMode::LINES) {
builder.withWireframe();
}
auto pipelineType = getPipelineType(mat->second);
if (pipelineType == Pipeline::MATERIAL) {
builder.withMaterial();
graphics::MaterialKey drawMaterialKey = mat->second.getMaterialKey();
if (drawMaterialKey.isNormalMap()) {
builder.withTangents();
}
if (drawMaterialKey.isLightMap()) {
builder.withLightMap();
}
if (drawMaterialKey.isUnlit()) {
builder.withUnlit();
}
builder.withCullFaceMode(mat->second.getCullFaceMode());
} else if (pipelineType == Pipeline::PROCEDURAL) {
builder.withOwnPipeline();
}
updateShapeKeyBuilderFromMaterials(builder);
return builder.build();
}
Item::Bound ShapeEntityRenderer::getBound(RenderArgs* args) {
auto mat = _materials.find("0");
if (mat != _materials.end() && mat->second.top().material && mat->second.top().material->isProcedural() &&
mat->second.top().material->isReady()) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(mat->second.top().material);
if (procedural->hasVertexShader() && procedural->hasBoundOperator()) {
return procedural->getBound(args);
}
}
return Parent::getBound(args);
}
void ShapeEntityRenderer::doRender(RenderArgs* args) {
PerformanceTimer perfTimer("RenderableShapeEntityItem::render");
Q_ASSERT(args->_batch);
gpu::Batch& batch = *args->_batch;
graphics::MultiMaterial materials;
auto geometryCache = DependencyManager::get<GeometryCache>();
GeometryCache::Shape geometryShape = geometryCache->getShapeForEntityShape(_shape);
glm::vec4 outColor;
Pipeline pipelineType;
Transform transform;
withReadLock([&] {
transform = _renderTransform;
{
std::lock_guard<std::mutex> lock(_materialsLock);
materials = _materials["0"];
pipelineType = getPipelineType(materials);
auto& schema = materials.getSchemaBuffer().get<graphics::MultiMaterial::Schema>();
outColor = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity);
});
}
auto& schema = materials.getSchemaBuffer().get<graphics::MultiMaterial::Schema>();
glm::vec4 outColor = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity);
outColor = EntityRenderer::calculatePulseColor(outColor, _pulseProperties, _created);
if (outColor.a == 0.0f) {
return;
}
gpu::Batch& batch = *args->_batch;
auto geometryCache = DependencyManager::get<GeometryCache>();
GeometryCache::Shape geometryShape = geometryCache->getShapeForEntityShape(_shape);
Transform transform;
withReadLock([&] {
transform = _renderTransform;
});
bool wireframe = render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES;
transform.setRotation(BillboardModeHelpers::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode,
args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE ? BillboardModeHelpers::getPrimaryViewFrustumPosition() : args->getViewFrustum().getPosition(),
_shape < entity::Shape::Cube || _shape > entity::Shape::Icosahedron));
batch.setModelTransform(transform);
Pipeline pipelineType = getPipelineType(materials);
if (pipelineType == Pipeline::PROCEDURAL) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(materials.top().material);
outColor = procedural->getColor(outColor);
@ -248,7 +132,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) {
procedural->prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(outColor.a < 1.0f));
});
if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) {
if (wireframe) {
geometryCache->renderWireShape(batch, geometryShape, outColor);
} else {
geometryCache->renderShape(batch, geometryShape, outColor);
@ -256,12 +140,21 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) {
} else if (pipelineType == Pipeline::SIMPLE) {
// FIXME, support instanced multi-shape rendering using multidraw indirect
outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
render::ShapePipelinePointer pipeline = geometryCache->getShapePipelinePointer(outColor.a < 1.0f, false,
_renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD, materials.top().material->getCullFaceMode());
if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) {
geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline);
bool forward = _renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD;
if (outColor.a >= 1.0f) {
render::ShapePipelinePointer pipeline = geometryCache->getShapePipelinePointer(false, wireframe || materials.top().material->isUnlit(),
forward, materials.top().material->getCullFaceMode());
if (wireframe) {
geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline);
} else {
geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline);
}
} else {
geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline);
if (wireframe) {
geometryCache->renderWireShape(batch, geometryShape, outColor);
} else {
geometryCache->renderShape(batch, geometryShape, outColor);
}
}
} else {
if (RenderPipelines::bindMaterials(materials, batch, args->_renderMode, args->_enableTexturing)) {

View file

@ -35,9 +35,6 @@ private:
virtual void doRender(RenderArgs* args) override;
virtual bool isTransparent() const override;
enum Pipeline { SIMPLE, MATERIAL, PROCEDURAL };
Pipeline getPipelineType(const graphics::MultiMaterial& materials) const;
QString _proceduralData;
entity::Shape _shape { entity::Sphere };

View file

@ -20,6 +20,7 @@
#include "GLMHelpers.h"
#include "DeferredLightingEffect.h"
#include "RenderPipelines.h"
using namespace render;
using namespace render::entities;
@ -35,6 +36,8 @@ TextEntityRenderer::TextEntityRenderer(const EntityItemPointer& entity) :
if (geometryCache) {
_geometryID = geometryCache->allocateID();
}
_material->setCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE);
addMaterial(graphics::MaterialLayer(_material, 0), "0");
}
TextEntityRenderer::~TextEntityRenderer() {
@ -44,41 +47,8 @@ TextEntityRenderer::~TextEntityRenderer() {
}
}
bool TextEntityRenderer::isTransparent() const {
return Parent::isTransparent() || _backgroundAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE;
}
bool TextEntityRenderer::isTextTransparent() const {
return resultWithReadLock<bool>([&] {
return Parent::isTransparent() || _textAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE;
});
}
ItemKey TextEntityRenderer::getKey() {
return ItemKey::Builder(Parent::getKey()).withMetaCullGroup();
}
ShapeKey TextEntityRenderer::getShapeKey() {
auto builder = render::ShapeKey::Builder().withoutCullFace();
if (isTransparent()) {
builder.withTranslucent();
}
if (_unlit) {
builder.withUnlit();
}
if (_primitiveMode == PrimitiveMode::LINES) {
builder.withWireframe();
}
return builder.build();
}
uint32_t TextEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) const {
auto parentSubs = Parent::metaFetchMetaSubItems(subItems);
if (Item::isValidID(_textRenderID)) {
subItems.emplace_back(_textRenderID);
return parentSubs + 1;
}
return parentSubs;
bool TextEntityRenderer::needsRenderUpdate() const {
return needsRenderUpdateFromMaterials() || Parent::needsRenderUpdate();
}
void TextEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
@ -98,58 +68,126 @@ void TextEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointe
_lineHeight = entity->getLineHeight();
_textColor = toGlm(entity->getTextColor());
_textAlpha = entity->getTextAlpha();
_backgroundColor = toGlm(entity->getBackgroundColor());
_backgroundAlpha = entity->getBackgroundAlpha();
_leftMargin = entity->getLeftMargin();
_rightMargin = entity->getRightMargin();
_topMargin = entity->getTopMargin();
_bottomMargin = entity->getBottomMargin();
_unlit = entity->getUnlit();
_font = entity->getFont();
_effect = entity->getTextEffect();
_effectColor = toGlm(entity->getTextEffectColor());
_effectThickness = entity->getTextEffectThickness();
_alignment = entity->getAlignment();
bool materialChanged = false;
glm::vec3 color = toGlm(entity->getBackgroundColor());
if (_backgroundColor != color) {
_backgroundColor = color;
_material->setAlbedo(color);
materialChanged = true;
}
float alpha = entity->getBackgroundAlpha();
if (_backgroundAlpha != alpha) {
_backgroundAlpha = alpha;
_material->setOpacity(alpha);
materialChanged = true;
}
auto unlit = entity->getUnlit();
if (_unlit != unlit) {
_unlit = unlit;
_material->setUnlit(_unlit);
materialChanged = true;
}
updateMaterials(materialChanged);
updateTextRenderItem();
}
bool TextEntityRenderer::isTransparent() const {
bool backgroundTransparent = _backgroundAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE;
return backgroundTransparent || Parent::isTransparent() || materialsTransparent();
}
bool TextEntityRenderer::isTextTransparent() const {
return Parent::isTransparent() || _textAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE;
}
Item::Bound TextEntityRenderer::getBound(RenderArgs* args) {
return Parent::getMaterialBound(args);
}
ItemKey TextEntityRenderer::getKey() {
return ItemKey::Builder(Parent::getKey()).withMetaCullGroup();
}
ShapeKey TextEntityRenderer::getShapeKey() {
auto builder = render::ShapeKey::Builder().withDepthBias();
updateShapeKeyBuilderFromMaterials(builder);
return builder.build();
}
uint32_t TextEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) const {
auto parentSubs = Parent::metaFetchMetaSubItems(subItems);
if (Item::isValidID(_textRenderID)) {
subItems.emplace_back(_textRenderID);
return parentSubs + 1;
}
return parentSubs;
}
void TextEntityRenderer::doRender(RenderArgs* args) {
PerformanceTimer perfTimer("RenderableTextEntityItem::render");
Q_ASSERT(args->_batch);
gpu::Batch& batch = *args->_batch;
glm::vec4 backgroundColor;
Transform transform;
withReadLock([&] {
transform = _renderTransform;
graphics::MultiMaterial materials;
{
std::lock_guard<std::mutex> lock(_materialsLock);
materials = _materials["0"];
}
float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
backgroundColor = glm::vec4(_backgroundColor, fadeRatio * _backgroundAlpha);
});
auto& schema = materials.getSchemaBuffer().get<graphics::MultiMaterial::Schema>();
glm::vec4 backgroundColor = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity);
backgroundColor = EntityRenderer::calculatePulseColor(backgroundColor, _pulseProperties, _created);
if (backgroundColor.a <= 0.0f) {
return;
}
gpu::Batch& batch = *args->_batch;
bool transparent;
Transform transform;
withReadLock([&] {
transparent = isTransparent();
transform = _renderTransform;
});
transform.setRotation(BillboardModeHelpers::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode,
args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE ? BillboardModeHelpers::getPrimaryViewFrustumPosition() : args->getViewFrustum().getPosition()));
batch.setModelTransform(transform);
Pipeline pipelineType = getPipelineType(materials);
if (pipelineType == Pipeline::PROCEDURAL) {
auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(materials.top().material);
transparent |= procedural->isFading();
procedural->prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(transparent));
} else if (pipelineType == Pipeline::MATERIAL) {
if (RenderPipelines::bindMaterials(materials, batch, args->_renderMode, args->_enableTexturing)) {
args->_details._materialSwitches++;
}
}
auto geometryCache = DependencyManager::get<GeometryCache>();
// FIXME: we want to use instanced rendering here, but if textAlpha < 1 and backgroundAlpha < 1, the transparency sorting will be wrong
//render::ShapePipelinePointer pipeline = geometryCache->getShapePipelinePointer(backgroundColor.a < 1.0f, _unlit,
// _renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD);
//if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) {
// geometryCache->renderWireShapeInstance(args, batch, GeometryCache::Quad, backgroundColor, pipeline);
//} else {
// geometryCache->renderSolidShapeInstance(args, batch, GeometryCache::Quad, backgroundColor, pipeline);
//}
if (pipelineType == Pipeline::SIMPLE) {
geometryCache->renderQuad(batch, glm::vec2(-0.5f), glm::vec2(0.5f), backgroundColor, _geometryID);
} else {
geometryCache->renderQuad(batch, glm::vec2(-0.5f), glm::vec2(0.5f), glm::vec2(0.0f), glm::vec2(1.0f), backgroundColor, _geometryID);
}
geometryCache->renderQuad(batch, glm::vec2(-0.5), glm::vec2(0.5), backgroundColor, _geometryID);
const int TRIANBLES_PER_QUAD = 2;
args->_details._trianglesRendered += TRIANBLES_PER_QUAD;
const int TRIANGLES_PER_QUAD = 2;
args->_details._trianglesRendered += TRIANGLES_PER_QUAD;
}
QSizeF TextEntityRenderer::textSize(const QString& text) const {

View file

@ -14,6 +14,8 @@
#include "RenderableEntityItem.h"
#include <procedural/Procedural.h>
class TextEntityItem;
class TextRenderer3D;
@ -33,6 +35,7 @@ public:
protected:
bool isTransparent() const override;
bool isTextTransparent() const;
Item::Bound getBound(RenderArgs* args) override;
ShapeKey getShapeKey() override;
ItemKey getKey() override;
virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) const override;
@ -41,6 +44,7 @@ protected:
void onRemoveFromSceneTyped(const TypedEntityPointer& entity) override;
private:
virtual bool needsRenderUpdate() const override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
virtual void doRender(RenderArgs* args) override;
@ -53,10 +57,12 @@ private:
float _lineHeight;
glm::vec3 _textColor;
float _textAlpha;
glm::vec3 _backgroundColor;
float _backgroundAlpha;
bool _unlit;
std::shared_ptr<graphics::ProceduralMaterial> _material { std::make_shared<graphics::ProceduralMaterial>() };
glm::vec3 _backgroundColor { NAN };
float _backgroundAlpha { NAN };
float _leftMargin;
float _rightMargin;
float _topMargin;

View file

@ -723,7 +723,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
*
* @property {Vec3} naturalPosition=0,0,0 - The center of the entity's unscaled mesh model if it has one, otherwise
* {@link Vec3(0)|Vec3.ZERO}. <em>Read-only.</em>
* @property {Vec3} naturalDimensions - The dimensions of the entity's unscaled mesh model if it has one, otherwise
* @property {Vec3} naturalDimensions - The dimensions of the entity's unscaled mesh model or image if it has one, otherwise
* {@link Vec3(0)|Vec3.ONE}. <em>Read-only.</em>
*
* @property {Vec3} velocity=0,0,0 - The linear velocity of the entity in m/s with respect to world coordinates.

View file

@ -57,6 +57,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership
connect(nodeList.data(), &NodeList::canRezTmpCertifiedChanged, this, &EntityScriptingInterface::canRezTmpCertifiedChanged);
connect(nodeList.data(), &NodeList::canWriteAssetsChanged, this, &EntityScriptingInterface::canWriteAssetsChanged);
connect(nodeList.data(), &NodeList::canGetAndSetPrivateUserDataChanged, this, &EntityScriptingInterface::canGetAndSetPrivateUserDataChanged);
connect(nodeList.data(), &NodeList::canRezAvatarEntitiesChanged, this, &EntityScriptingInterface::canRezAvatarEntitiesChanged);
auto& packetReceiver = nodeList->getPacketReceiver();
packetReceiver.registerListener(PacketType::EntityScriptCallMethod,
@ -114,6 +115,11 @@ bool EntityScriptingInterface::canGetAndSetPrivateUserData() {
return nodeList->getThisNodeCanGetAndSetPrivateUserData();
}
bool EntityScriptingInterface::canRezAvatarEntities() {
auto nodeList = DependencyManager::get<NodeList>();
return nodeList->getThisNodeCanRezAvatarEntities();
}
void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) {
if (_entityTree) {
disconnect(_entityTree.get(), &EntityTree::addingEntityPointer, this, &EntityScriptingInterface::onAddingEntity);
@ -481,6 +487,15 @@ QUuid EntityScriptingInterface::addEntityInternal(const EntityItemProperties& pr
_activityTracking.addedEntityCount++;
auto nodeList = DependencyManager::get<NodeList>();
if (entityHostType == entity::HostType::AVATAR && !nodeList->getThisNodeCanRezAvatarEntities()) {
qCDebug(entities) << "Ignoring addEntity() because don't have canRezAvatarEntities permission on domain";
// Only need to intercept methods that may add an avatar entity because avatar entities are removed from the tree when
// the user doesn't have canRezAvatarEntities permission.
return QUuid();
}
EntityItemProperties propertiesWithSimID = properties;
propertiesWithSimID.setEntityHostType(entityHostType);
if (entityHostType == entity::HostType::AVATAR) {
@ -493,7 +508,6 @@ QUuid EntityScriptingInterface::addEntityInternal(const EntityItemProperties& pr
}
// the created time will be set in EntityTree::addEntity by recordCreationTime()
auto nodeList = DependencyManager::get<NodeList>();
auto sessionID = nodeList->getSessionUUID();
propertiesWithSimID.setLastEditedBy(sessionID);
@ -1002,7 +1016,7 @@ void EntityScriptingInterface::deleteEntity(const QUuid& id) {
for (auto entity : entitiesToDeleteImmediately) {
if (entity->isMyAvatarEntity()) {
getEntityPacketSender()->getMyAvatar()->clearAvatarEntity(entity->getID(), false);
getEntityPacketSender()->getMyAvatar()->clearAvatarEntityInternal(entity->getID());
}
}
}

View file

@ -289,6 +289,14 @@ public slots:
* <code>privateUserData</code> property of entities, otherwise <code>false</code>.
*/
Q_INVOKABLE bool canGetAndSetPrivateUserData();
/**jsdoc
* Checks whether or not the script can rez avatar entities.
* @function Entities.canRezAvatarEntities
* @returns {boolean} <code>true</code> if the domain server will allow the script to rez avatar entities,
* otherwise <code>false</code>.
*/
Q_INVOKABLE bool canRezAvatarEntities();
/**jsdoc
* <p>How an entity is hosted and sent to others for display.</p>
@ -2255,11 +2263,20 @@ signals:
/**jsdoc
* Triggered when your ability to get and set private user data changes.
* @function Entities.canGetAndSetPrivateUserDataChanged
* @param {boolean} canGetAndSetPrivateUserData - <code>true</code> if the script change the <code>privateUserData</code>
* @param {boolean} canGetAndSetPrivateUserData - <code>true</code> if the script can change the <code>privateUserData</code>
* property of an entity, <code>false</code> if it can't.
* @returns {Signal}
*/
void canGetAndSetPrivateUserDataChanged(bool canGetAndSetPrivateUserData);
/**jsdoc
* Triggered when your ability to use avatar entities is changed.
* @function Entities.canRezAvatarEntitiesChanged
* @param {boolean} canRezAvatarEntities - <code>true</code> if the script can change edit avatar entities,
* <code>false</code> if it can't.
* @returns {Signal}
*/
void canRezAvatarEntitiesChanged(bool canRezAvatarEntities);
/**jsdoc

View file

@ -34,6 +34,7 @@ EntityItemProperties ImageEntityItem::getProperties(const EntityPropertyFlags& d
COPY_ENTITY_PROPERTY_TO_PROPERTIES(alpha, getAlpha);
withReadLock([&] {
_pulseProperties.getProperties(properties);
properties.setNaturalDimensions(_naturalDimensions);
});
COPY_ENTITY_PROPERTY_TO_PROPERTIES(imageURL, getImageURL);
@ -217,4 +218,10 @@ PulsePropertyGroup ImageEntityItem::getPulseProperties() const {
return resultWithReadLock<PulsePropertyGroup>([&] {
return _pulseProperties;
});
}
}
void ImageEntityItem::setNaturalDimension(const glm::vec3& naturalDimensions) const {
withWriteLock([&] {
_naturalDimensions = naturalDimensions;
});
}

View file

@ -63,6 +63,8 @@ public:
PulsePropertyGroup getPulseProperties() const;
void setNaturalDimension(const glm::vec3& naturalDimensions) const;
protected:
glm::u8vec3 _color;
float _alpha;
@ -72,6 +74,8 @@ protected:
bool _emissive { false };
bool _keepAspectRatio { true };
QRect _subImage;
mutable glm::vec3 _naturalDimensions;
};
#endif // hifi_ImageEntityItem_h

View file

@ -339,7 +339,7 @@ bool ShapeEntityItem::findDetailedParabolaIntersection(const glm::vec3& origin,
bool ShapeEntityItem::getRotateForPicking() const {
auto shape = getShape();
return getBillboardMode() != BillboardMode::NONE && (_shape < entity::Shape::Cube || _shape > entity::Shape::Icosahedron);
return getBillboardMode() != BillboardMode::NONE && (shape < entity::Shape::Cube || shape > entity::Shape::Icosahedron);
}
void ShapeEntityItem::debugDump() const {

View file

@ -1863,7 +1863,7 @@ void GLTFSerializer::setHFMMaterial(HFMMaterial& hfmMat, const GLTFMaterial& mat
template<typename T, typename L>
bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int count,
QVector<L>& outarray, int accessorType) {
QVector<L>& outarray, int accessorType, bool normalized) {
QDataStream blobstream(bin);
blobstream.setByteOrder(QDataStream::LittleEndian);
@ -1899,12 +1899,22 @@ bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int c
blobstream.setDevice(nullptr);
return false;
}
float scale = 1.0f; // Normalized output values should always be floats.
if (normalized) {
scale = (float)(std::numeric_limits<T>::max)();
}
for (int i = 0; i < count; ++i) {
for (int j = 0; j < bufferCount; ++j) {
if (!blobstream.atEnd()) {
T value;
blobstream >> value;
outarray.push_back(value);
if (normalized) {
outarray.push_back(std::max((float)value / scale, -1.0f));
} else {
outarray.push_back(value);
}
} else {
blobstream.setDevice(nullptr);
return false;
@ -1917,24 +1927,24 @@ bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int c
}
template<typename T>
bool GLTFSerializer::addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count,
QVector<T>& outarray, int accessorType, int componentType) {
QVector<T>& outarray, int accessorType, int componentType, bool normalized) {
switch (componentType) {
case GLTFAccessorComponentType::BYTE: {}
case GLTFAccessorComponentType::UNSIGNED_BYTE: {
return readArray<uchar>(bin, byteOffset, count, outarray, accessorType);
return readArray<uchar>(bin, byteOffset, count, outarray, accessorType, normalized);
}
case GLTFAccessorComponentType::SHORT: {
return readArray<short>(bin, byteOffset, count, outarray, accessorType);
return readArray<short>(bin, byteOffset, count, outarray, accessorType, normalized);
}
case GLTFAccessorComponentType::UNSIGNED_INT: {
return readArray<uint>(bin, byteOffset, count, outarray, accessorType);
return readArray<uint>(bin, byteOffset, count, outarray, accessorType, normalized);
}
case GLTFAccessorComponentType::UNSIGNED_SHORT: {
return readArray<ushort>(bin, byteOffset, count, outarray, accessorType);
return readArray<ushort>(bin, byteOffset, count, outarray, accessorType, normalized);
}
case GLTFAccessorComponentType::FLOAT: {
return readArray<float>(bin, byteOffset, count, outarray, accessorType);
return readArray<float>(bin, byteOffset, count, outarray, accessorType, normalized);
}
}
return false;
@ -1951,11 +1961,11 @@ bool GLTFSerializer::addArrayFromAccessor(GLTFAccessor& accessor, QVector<T>& ou
int accBoffset = accessor.defined["byteOffset"] ? accessor.byteOffset : 0;
success = addArrayOfType(buffer.blob, bufferview.byteOffset + accBoffset, accessor.count, outarray, accessor.type,
accessor.componentType);
accessor.componentType, accessor.normalized);
} else {
for (int i = 0; i < accessor.count; ++i) {
T value;
memset(&value, 0, sizeof(T)); // Make sure the dummy array is initalised to zero.
memset(&value, 0, sizeof(T)); // Make sure the dummy array is initialized to zero.
outarray.push_back(value);
}
}
@ -1971,7 +1981,7 @@ bool GLTFSerializer::addArrayFromAccessor(GLTFAccessor& accessor, QVector<T>& ou
success = addArrayOfType(sparseIndicesBuffer.blob, sparseIndicesBufferview.byteOffset + accSIBoffset,
accessor.sparse.count, out_sparse_indices_array, GLTFAccessorType::SCALAR,
accessor.sparse.indices.componentType);
accessor.sparse.indices.componentType, false);
if (success) {
QVector<T> out_sparse_values_array;
@ -1981,7 +1991,8 @@ bool GLTFSerializer::addArrayFromAccessor(GLTFAccessor& accessor, QVector<T>& ou
int accSVBoffset = accessor.sparse.values.defined["byteOffset"] ? accessor.sparse.values.byteOffset : 0;
success = addArrayOfType(sparseValuesBuffer.blob, sparseValuesBufferview.byteOffset + accSVBoffset,
accessor.sparse.count, out_sparse_values_array, accessor.type, accessor.componentType);
accessor.sparse.count, out_sparse_values_array, accessor.type, accessor.componentType,
accessor.normalized);
if (success) {
for (int i = 0; i < accessor.sparse.count; ++i) {

View file

@ -527,7 +527,7 @@ struct GLTFAccessor {
int componentType; //required
int count; //required
int type; //required
bool normalized{ false };
bool normalized { false };
QVector<double> max;
QVector<double> min;
GLTFAccessorSparse sparse;
@ -832,11 +832,11 @@ private:
template<typename T, typename L>
bool readArray(const hifi::ByteArray& bin, int byteOffset, int count,
QVector<L>& outarray, int accessorType);
QVector<L>& outarray, int accessorType, bool normalized);
template<typename T>
bool addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count,
QVector<T>& outarray, int accessorType, int componentType);
QVector<T>& outarray, int accessorType, int componentType, bool normalized);
template <typename T>
bool addArrayFromAccessor(GLTFAccessor& accessor, QVector<T>& outarray);

View file

@ -840,10 +840,7 @@ void GLBackend::do_glColor4f(const Batch& batch, size_t paramOffset) {
if (_input._colorAttribute != newColor) {
_input._colorAttribute = newColor;
glVertexAttrib4fv(gpu::Stream::COLOR, &_input._colorAttribute.r);
// Color has been changed and is not white. To prevent colors from bleeding
// between different objects, we need to set the _hadColorAttribute flag
// as if a previous render call had potential colors
_input._hadColorAttribute = (newColor != glm::vec4(1.0f, 1.0f, 1.0f, 1.0f));
_input._hasColorAttribute = true;
}
(void)CHECK_GL_ERROR();
}

View file

@ -348,36 +348,37 @@ protected:
virtual void updateInput() = 0;
struct InputStageState {
bool _invalidFormat{ true };
bool _lastUpdateStereoState{ false };
bool _hadColorAttribute{ true };
FormatReference _format{ GPU_REFERENCE_INIT_VALUE };
bool _invalidFormat { true };
bool _lastUpdateStereoState { false };
bool _hasColorAttribute { false };
bool _hadColorAttribute { false };
FormatReference _format { GPU_REFERENCE_INIT_VALUE };
std::string _formatKey;
typedef std::bitset<MAX_NUM_ATTRIBUTES> ActivationCache;
ActivationCache _attributeActivation{ 0 };
ActivationCache _attributeActivation { 0 };
typedef std::bitset<MAX_NUM_INPUT_BUFFERS> BuffersState;
BuffersState _invalidBuffers{ 0 };
BuffersState _attribBindingBuffers{ 0 };
BuffersState _invalidBuffers { 0 };
BuffersState _attribBindingBuffers { 0 };
std::array<BufferReference, MAX_NUM_INPUT_BUFFERS> _buffers{};
std::array<Offset, MAX_NUM_INPUT_BUFFERS> _bufferOffsets{};
std::array<Offset, MAX_NUM_INPUT_BUFFERS> _bufferStrides{};
std::array<GLuint, MAX_NUM_INPUT_BUFFERS> _bufferVBOs{};
std::array<BufferReference, MAX_NUM_INPUT_BUFFERS> _buffers;
std::array<Offset, MAX_NUM_INPUT_BUFFERS> _bufferOffsets;
std::array<Offset, MAX_NUM_INPUT_BUFFERS> _bufferStrides;
std::array<GLuint, MAX_NUM_INPUT_BUFFERS> _bufferVBOs;
glm::vec4 _colorAttribute{ 0.0f };
glm::vec4 _colorAttribute { 1.0f };
BufferReference _indexBuffer{};
Offset _indexBufferOffset{ 0 };
Type _indexBufferType{ UINT32 };
BufferReference _indexBuffer;
Offset _indexBufferOffset { 0 };
Type _indexBufferType { UINT32 };
BufferReference _indirectBuffer{};
Offset _indirectBufferOffset{ 0 };
Offset _indirectBufferStride{ 0 };
BufferReference _indirectBuffer;
Offset _indirectBufferOffset { 0 };
Offset _indirectBufferStride { 0 };
GLuint _defaultVAO{ 0 };
GLuint _defaultVAO { 0 };
} _input;
virtual void initTransform() = 0;

View file

@ -103,6 +103,9 @@ void GLBackend::resetInputStage() {
reset(_input._format);
_input._formatKey.clear();
_input._invalidFormat = false;
_input._hasColorAttribute = false;
_input._hadColorAttribute = false;
_input._colorAttribute = vec4(1.0f);
_input._attributeActivation.reset();
for (uint32_t i = 0; i < _input._buffers.size(); i++) {
@ -159,15 +162,15 @@ void GLBackend::updateInput() {
_input._invalidFormat |= (isStereoNow != _input._lastUpdateStereoState);
#endif
_input._lastUpdateStereoState = isStereoNow;
bool hasColorAttribute = _input._hasColorAttribute;
if (_input._invalidFormat) {
InputStageState::ActivationCache newActivation;
// Assign the vertex format required
auto format = acquire(_input._format);
if (format) {
bool hasColorAttribute{ false };
_input._attribBindingBuffers.reset();
const auto& attributes = format->getAttributes();
@ -186,12 +189,12 @@ void GLBackend::updateInput() {
uint8_t locationCount = attrib._element.getLocationCount();
GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()];
GLuint offset = (GLuint)attrib._offset;;
GLuint offset = (GLuint)attrib._offset;
GLboolean isNormalized = attrib._element.isNormalized();
GLenum perLocationSize = attrib._element.getLocationSize();
hasColorAttribute = hasColorAttribute || (slot == Stream::COLOR);
hasColorAttribute |= slot == Stream::COLOR;
for (GLuint locNum = 0; locNum < locationCount; ++locNum) {
GLuint attriNum = (GLuint)(slot + locNum);
@ -224,14 +227,11 @@ void GLBackend::updateInput() {
#endif
}
if (_input._hadColorAttribute && !hasColorAttribute) {
// The previous input stage had a color attribute but this one doesn't so reset
// color to pure white.
const auto white = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
glVertexAttrib4fv(Stream::COLOR, &white.r);
_input._colorAttribute = white;
if (!hasColorAttribute && _input._hadColorAttribute) {
// The previous input stage had a color attribute but this one doesn't, so reset the color to pure white.
_input._colorAttribute = glm::vec4(1.0f);
glVertexAttrib4fv(Stream::COLOR, &_input._colorAttribute.r);
}
_input._hadColorAttribute = hasColorAttribute;
}
// Manage Activation what was and what is expected now
@ -253,6 +253,9 @@ void GLBackend::updateInput() {
_stats._ISNumFormatChanges++;
}
_input._hadColorAttribute = hasColorAttribute;
_input._hasColorAttribute = false;
if (_input._invalidBuffers.any()) {
auto vbo = _input._bufferVBOs.data();
auto offset = _input._bufferOffsets.data();
@ -276,4 +279,3 @@ void GLBackend::updateInput() {
(void)CHECK_GL_ERROR();
}
}

View file

@ -1,7 +1,7 @@
set(TARGET_NAME gpu-gl)
setup_hifi_library(Concurrent)
link_hifi_libraries(shared gl gpu gpu-gl-common shaders)
if (UNIX)
if (UNIX AND NOT VIRCADIA_THREAD_DEBUGGING)
target_link_libraries(${TARGET_NAME} pthread)
endif(UNIX)
GroupSources("src")

View file

@ -33,6 +33,8 @@ void GL41Backend::updateInput() {
#endif
_input._lastUpdateStereoState = isStereoNow;
bool hasColorAttribute = _input._hasColorAttribute;
if (_input._invalidFormat || _input._invalidBuffers.any()) {
auto format = acquire(_input._format);
@ -71,8 +73,6 @@ void GL41Backend::updateInput() {
// now we need to bind the buffers and assign the attrib pointers
if (format) {
bool hasColorAttribute{ false };
const auto& buffers = _input._buffers;
const auto& offsets = _input._bufferOffsets;
const auto& strides = _input._bufferStrides;
@ -110,7 +110,7 @@ void GL41Backend::updateInput() {
uintptr_t pointer = (uintptr_t)(attrib._offset + offsets[bufferNum]);
GLboolean isNormalized = attrib._element.isNormalized();
hasColorAttribute = hasColorAttribute || (slot == Stream::COLOR);
hasColorAttribute |= slot == Stream::COLOR;
for (size_t locNum = 0; locNum < locationCount; ++locNum) {
if (attrib._element.isInteger()) {
@ -132,17 +132,16 @@ void GL41Backend::updateInput() {
}
}
if (_input._hadColorAttribute && !hasColorAttribute) {
// The previous input stage had a color attribute but this one doesn't so reset
// color to pure white.
const auto white = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
glVertexAttrib4fv(Stream::COLOR, &white.r);
_input._colorAttribute = white;
if (!hasColorAttribute && _input._hadColorAttribute) {
// The previous input stage had a color attribute but this one doesn't, so reset the color to pure white.
_input._colorAttribute = glm::vec4(1.0f);
glVertexAttrib4fv(Stream::COLOR, &_input._colorAttribute.r);
}
_input._hadColorAttribute = hasColorAttribute;
}
// everything format related should be in sync now
_input._invalidFormat = false;
}
}
_input._hadColorAttribute = hasColorAttribute;
_input._hasColorAttribute = false;
}

View file

@ -35,14 +35,14 @@ void GL45Backend::updateInput() {
#endif
_input._lastUpdateStereoState = isStereoNow;
bool hasColorAttribute = _input._hasColorAttribute;
if (_input._invalidFormat) {
InputStageState::ActivationCache newActivation;
// Assign the vertex format required
auto format = acquire(_input._format);
if (format) {
bool hasColorAttribute{ false };
_input._attribBindingBuffers.reset();
const auto& attributes = format->getAttributes();
@ -61,12 +61,12 @@ void GL45Backend::updateInput() {
uint8_t locationCount = attrib._element.getLocationCount();
GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()];
GLuint offset = (GLuint)attrib._offset;;
GLuint offset = (GLuint)attrib._offset;
GLboolean isNormalized = attrib._element.isNormalized();
GLenum perLocationSize = attrib._element.getLocationSize();
hasColorAttribute = hasColorAttribute || (slot == Stream::COLOR);
hasColorAttribute |= slot == Stream::COLOR;
for (GLuint locNum = 0; locNum < locationCount; ++locNum) {
GLuint attriNum = (GLuint)(slot + locNum);
@ -99,14 +99,11 @@ void GL45Backend::updateInput() {
#endif
}
if (_input._hadColorAttribute && !hasColorAttribute) {
// The previous input stage had a color attribute but this one doesn't so reset
// color to pure white.
const auto white = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
glVertexAttrib4fv(Stream::COLOR, &white.r);
_input._colorAttribute = white;
if (!hasColorAttribute && _input._hadColorAttribute) {
// The previous input stage had a color attribute but this one doesn't, so reset the color to pure white.
_input._colorAttribute = glm::vec4(1.0f);
glVertexAttrib4fv(Stream::COLOR, &_input._colorAttribute.r);
}
_input._hadColorAttribute = hasColorAttribute;
}
// Manage Activation what was and what is expected now
@ -128,6 +125,9 @@ void GL45Backend::updateInput() {
_stats._ISNumFormatChanges++;
}
_input._hadColorAttribute = hasColorAttribute;
_input._hasColorAttribute = false;
if (_input._invalidBuffers.any()) {
auto vbo = _input._bufferVBOs.data();
auto offset = _input._bufferOffsets.data();

View file

@ -579,11 +579,11 @@ public:
ExternalUpdates getUpdates() const;
// Serialize a texture into a KTX file
static ktx::KTXUniquePointer serialize(const Texture& texture);
static ktx::KTXUniquePointer serialize(const Texture& texture, const glm::ivec2& originalSize);
static TexturePointer build(const ktx::KTXDescriptor& descriptor);
static TexturePointer unserialize(const std::string& ktxFile);
static TexturePointer unserialize(const cache::FilePointer& cacheEntry, const std::string& source = std::string());
static std::pair<TexturePointer, glm::ivec2> build(const ktx::KTXDescriptor& descriptor);
static std::pair<TexturePointer, glm::ivec2> unserialize(const std::string& ktxFile);
static std::pair<TexturePointer, glm::ivec2> unserialize(const cache::FilePointer& cacheEntry, const std::string& source = std::string());
static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header);
static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat);

View file

@ -13,6 +13,7 @@
#include "Texture.h"
#include <QtCore/QByteArray>
#include <glm/gtc/type_ptr.hpp>
#include <ktx/KTX.h>
@ -30,15 +31,16 @@ struct GPUKTXPayload {
using Version = uint8;
static const std::string KEY;
static const Version CURRENT_VERSION { 1 };
static const Version CURRENT_VERSION { 2 };
static const size_t PADDING { 2 };
static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32) + sizeof(TextureUsageType) + PADDING };
static_assert(GPUKTXPayload::SIZE == 36, "Packing size may differ between platforms");
static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32) + sizeof(TextureUsageType) + sizeof(glm::ivec2) + PADDING };
static_assert(GPUKTXPayload::SIZE == 44, "Packing size may differ between platforms");
static_assert(GPUKTXPayload::SIZE % 4 == 0, "GPUKTXPayload is not 4 bytes aligned");
Sampler::Desc _samplerDesc;
Texture::Usage _usage;
TextureUsageType _usageType;
glm::ivec2 _originalSize { 0, 0 };
Byte* serialize(Byte* data) const {
*(Version*)data = CURRENT_VERSION;
@ -56,26 +58,22 @@ struct GPUKTXPayload {
memcpy(data, &_usageType, sizeof(TextureUsageType));
data += sizeof(TextureUsageType);
memcpy(data, glm::value_ptr(_originalSize), sizeof(glm::ivec2));
data += sizeof(glm::ivec2);
return data + PADDING;
}
bool unserialize(const Byte* data, size_t size) {
if (size != SIZE) {
Version version = *(const Version*)data;
data += sizeof(Version);
if (version > CURRENT_VERSION) {
// If we try to load a version that we don't know how to parse,
// it will render incorrectly
return false;
}
Version version = *(const Version*)data;
if (version != CURRENT_VERSION) {
glm::vec4 borderColor(1.0f);
if (memcmp(&borderColor, data, sizeof(glm::vec4)) == 0) {
memcpy(this, data, sizeof(GPUKTXPayload));
return true;
} else {
return false;
}
}
data += sizeof(Version);
memcpy(&_samplerDesc, data, sizeof(Sampler::Desc));
data += sizeof(Sampler::Desc);
@ -87,6 +85,13 @@ struct GPUKTXPayload {
data += sizeof(uint32);
memcpy(&_usageType, data, sizeof(TextureUsageType));
data += sizeof(TextureUsageType);
if (version >= 2) {
memcpy(&_originalSize, data, sizeof(glm::ivec2));
data += sizeof(glm::ivec2);
}
return true;
}
@ -382,7 +387,7 @@ void Texture::setKtxBacking(const cache::FilePointer& cacheEntry) {
}
ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
ktx::KTXUniquePointer Texture::serialize(const Texture& texture, const glm::ivec2& originalSize) {
ktx::Header header;
// From texture format to ktx format description
@ -459,6 +464,7 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
gpuKeyval._samplerDesc = texture.getSampler().getDesc();
gpuKeyval._usage = texture.getUsage();
gpuKeyval._usageType = texture.getUsageType();
gpuKeyval._originalSize = originalSize;
Byte keyvalPayload[GPUKTXPayload::SIZE];
gpuKeyval.serialize(keyvalPayload);
@ -514,19 +520,19 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
return ktxBuffer;
}
TexturePointer Texture::build(const ktx::KTXDescriptor& descriptor) {
std::pair<TexturePointer, glm::ivec2> Texture::build(const ktx::KTXDescriptor& descriptor) {
Format mipFormat = Format::COLOR_BGRA_32;
Format texelFormat = Format::COLOR_SRGBA_32;
const auto& header = descriptor.header;
if (!Texture::evalTextureFormat(header, mipFormat, texelFormat)) {
return nullptr;
return { nullptr, { 0, 0 } };
}
// Find Texture Type based on dimensions
Type type = TEX_1D;
if (header.pixelWidth == 0) {
return nullptr;
return { nullptr, { 0, 0 } };
} else if (header.pixelHeight == 0) {
type = TEX_1D;
} else if (header.pixelDepth == 0) {
@ -569,39 +575,39 @@ TexturePointer Texture::build(const ktx::KTXDescriptor& descriptor) {
texture->overrideIrradiance(std::make_shared<SphericalHarmonics>(irradianceKtxKeyValue._irradianceSH));
}
return texture;
return { texture, gpuktxKeyValue._originalSize };
}
TexturePointer Texture::unserialize(const cache::FilePointer& cacheEntry, const std::string& source) {
std::pair<TexturePointer, glm::ivec2> Texture::unserialize(const cache::FilePointer& cacheEntry, const std::string& source) {
std::unique_ptr<ktx::KTX> ktxPointer = ktx::KTX::create(std::make_shared<storage::FileStorage>(cacheEntry->getFilepath().c_str()));
if (!ktxPointer) {
return nullptr;
return { nullptr, { 0, 0 } };
}
auto texture = build(ktxPointer->toDescriptor());
if (texture) {
texture->setKtxBacking(cacheEntry);
if (texture->source().empty()) {
texture->setSource(source);
auto textureAndSize = build(ktxPointer->toDescriptor());
if (textureAndSize.first) {
textureAndSize.first->setKtxBacking(cacheEntry);
if (textureAndSize.first->source().empty()) {
textureAndSize.first->setSource(source);
}
}
return texture;
return { textureAndSize.first, textureAndSize.second };
}
TexturePointer Texture::unserialize(const std::string& ktxfile) {
std::pair<TexturePointer, glm::ivec2> Texture::unserialize(const std::string& ktxfile) {
std::unique_ptr<ktx::KTX> ktxPointer = ktx::KTX::create(std::make_shared<storage::FileStorage>(ktxfile.c_str()));
if (!ktxPointer) {
return nullptr;
return { nullptr, { 0, 0 } };
}
auto texture = build(ktxPointer->toDescriptor());
if (texture) {
texture->setKtxBacking(ktxfile);
texture->setSource(ktxfile);
auto textureAndSize = build(ktxPointer->toDescriptor());
if (textureAndSize.first) {
textureAndSize.first->setKtxBacking(ktxfile);
textureAndSize.first->setSource(ktxfile);
}
return texture;
return { textureAndSize.first, textureAndSize.second };
}
bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) {

View file

@ -338,9 +338,9 @@ void mapToRedChannel(Image& image, ColorChannel sourceChannel) {
}
}
gpu::TexturePointer processImage(std::shared_ptr<QIODevice> content, const std::string& filename, ColorChannel sourceChannel,
int maxNumPixels, TextureUsage::Type textureType,
bool compress, BackendTarget target, const std::atomic<bool>& abortProcessing) {
std::pair<gpu::TexturePointer, glm::ivec2> processImage(std::shared_ptr<QIODevice> content, const std::string& filename, ColorChannel sourceChannel,
int maxNumPixels, TextureUsage::Type textureType,
bool compress, BackendTarget target, const std::atomic<bool>& abortProcessing) {
Image image = processRawImageData(*content.get(), filename);
// Texture content can take up a lot of memory. Here we release our ownership of that content
@ -354,7 +354,7 @@ gpu::TexturePointer processImage(std::shared_ptr<QIODevice> content, const std::
if (imageWidth == 0 || imageHeight == 0 || image.getFormat() == Image::Format_Invalid) {
QString reason(image.getFormat() == Image::Format_Invalid ? "(Invalid Format)" : "(Size is invalid)");
qCWarning(imagelogging) << "Failed to load:" << qPrintable(reason);
return nullptr;
return { nullptr, { imageWidth, imageHeight } };
}
// Validate the image is less than _maxNumPixels, and downscale if necessary
@ -378,7 +378,7 @@ gpu::TexturePointer processImage(std::shared_ptr<QIODevice> content, const std::
auto loader = TextureUsage::getTextureLoaderForType(textureType);
auto texture = loader(std::move(image), filename, compress, target, abortProcessing);
return texture;
return { texture, { imageWidth, imageHeight } };
}
Image processSourceImage(Image&& srcImage, bool cubemap, BackendTarget target) {

View file

@ -121,9 +121,9 @@ gpu::TexturePointer processCubeTextureColorFromImage(Image&& srcImage, const std
const QStringList getSupportedFormats();
gpu::TexturePointer processImage(std::shared_ptr<QIODevice> content, const std::string& url, ColorChannel sourceChannel,
int maxNumPixels, TextureUsage::Type textureType,
bool compress, gpu::BackendTarget target, const std::atomic<bool>& abortProcessing = false);
std::pair<gpu::TexturePointer, glm::ivec2> processImage(std::shared_ptr<QIODevice> content, const std::string& url, ColorChannel sourceChannel,
int maxNumPixels, TextureUsage::Type textureType,
bool compress, gpu::BackendTarget target, const std::atomic<bool>& abortProcessing = false);
void convertToTextureWithMips(gpu::Texture* texture, Image&& image, gpu::BackendTarget target, const std::atomic<bool>& abortProcessing = false, int face = -1);
void convertToTexture(gpu::Texture* texture, Image&& image, gpu::BackendTarget target, const std::atomic<bool>& abortProcessing = false, int face = -1, int mipLevel = 0);

View file

@ -19,7 +19,7 @@ using FilePointer = cache::FilePointer;
// Whenever a change is made to the serialized format for the KTX cache that isn't backward compatible,
// this value should be incremented. This will force the KTX cache to be wiped
const int KTXCache::CURRENT_VERSION = 0x01;
const int KTXCache::CURRENT_VERSION = 0x02;
const int KTXCache::INVALID_VERSION = 0x00;
const char* KTXCache::SETTING_VERSION_NAME = "hifi.ktx.cache_version";

View file

@ -266,23 +266,24 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs
return ResourceCache::getResource(modifiedUrl, QUrl(), &extra, std::hash<TextureExtra>()(extra)).staticCast<NetworkTexture>();
}
gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) {
std::weak_ptr<gpu::Texture> weakPointer;
std::pair<gpu::TexturePointer, glm::ivec2> TextureCache::getTextureByHash(const std::string& hash) {
std::pair<gpu::TextureWeakPointer, glm::ivec2> weakPointer;
{
std::unique_lock<std::mutex> lock(_texturesByHashesMutex);
weakPointer = _texturesByHashes[hash];
}
return weakPointer.lock();
return { weakPointer.first.lock(), weakPointer.second };
}
gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture) {
gpu::TexturePointer result;
std::pair<gpu::TexturePointer, glm::ivec2> TextureCache::cacheTextureByHash(const std::string& hash, const std::pair<gpu::TexturePointer, glm::ivec2>& textureAndSize) {
std::pair<gpu::TexturePointer, glm::ivec2> result;
{
std::unique_lock<std::mutex> lock(_texturesByHashesMutex);
result = _texturesByHashes[hash].lock();
if (!result) {
_texturesByHashes[hash] = texture;
result = texture;
auto& value = _texturesByHashes[hash];
result = { value.first.lock(), value.second };
if (!result.first) {
_texturesByHashes[hash] = textureAndSize;
result = textureAndSize;
}
}
return result;
@ -616,7 +617,7 @@ void NetworkTexture::makeLocalRequest() {
ktxDescriptor = std::make_shared<ktx::KTXDescriptor>(ktxFile->toDescriptor());
}
gpu::TexturePointer texture;
std::pair<gpu::TexturePointer, glm::ivec2> textureAndSize;
if (ktxDescriptor) {
std::string hash;
// Create bare ktx in memory
@ -634,18 +635,18 @@ void NetworkTexture::makeLocalRequest() {
}
auto textureCache = DependencyManager::get<TextureCache>();
texture = textureCache->getTextureByHash(hash);
if (!texture) {
texture = gpu::Texture::build(*ktxDescriptor);
if (texture) {
texture->setKtxBacking(path.toStdString());
texture->setSource(path.toStdString());
texture = textureCache->cacheTextureByHash(hash, texture);
textureAndSize = textureCache->getTextureByHash(hash);
if (!textureAndSize.first) {
textureAndSize = gpu::Texture::build(*ktxDescriptor);
if (textureAndSize.first) {
textureAndSize.first->setKtxBacking(path.toStdString());
textureAndSize.first->setSource(path.toStdString());
textureAndSize = textureCache->cacheTextureByHash(hash, textureAndSize);
}
}
}
if (!texture) {
if (!textureAndSize.first) {
qCDebug(networking).noquote() << "Failed load local KTX from" << path;
QMetaObject::invokeMethod(this, "setImage",
Q_ARG(gpu::TexturePointer, nullptr),
@ -655,11 +656,11 @@ void NetworkTexture::makeLocalRequest() {
}
_ktxResourceState = PENDING_MIP_REQUEST;
_lowestKnownPopulatedMip = texture->minAvailableMipLevel();
_lowestKnownPopulatedMip = textureAndSize.first->minAvailableMipLevel();
QMetaObject::invokeMethod(this, "setImage",
Q_ARG(gpu::TexturePointer, texture),
Q_ARG(int, texture->getWidth()),
Q_ARG(int, texture->getHeight()));
Q_ARG(gpu::TexturePointer, textureAndSize.first),
Q_ARG(int, textureAndSize.second.x),
Q_ARG(int, textureAndSize.second.y));
}
@ -968,22 +969,22 @@ void NetworkTexture::handleFinishedInitialLoad() {
auto textureCache = DependencyManager::get<TextureCache>();
gpu::TexturePointer texture = textureCache->getTextureByHash(hash);
std::pair<gpu::TexturePointer, glm::ivec2> textureAndSize = textureCache->getTextureByHash(hash);
if (!texture) {
if (!textureAndSize.first) {
auto ktxFile = textureCache->_ktxCache->getFile(hash);
if (ktxFile) {
texture = gpu::Texture::unserialize(ktxFile);
if (texture) {
texture = textureCache->cacheTextureByHash(hash, texture);
if (texture->source().empty()) {
texture->setSource(url.toString().toStdString());
textureAndSize = gpu::Texture::unserialize(ktxFile);
if (textureAndSize.first) {
textureAndSize = textureCache->cacheTextureByHash(hash, textureAndSize);
if (textureAndSize.first->source().empty()) {
textureAndSize.first->setSource(url.toString().toStdString());
}
}
}
}
if (!texture) {
if (!textureAndSize.first) {
auto memKtx = ktx::KTX::createBare(*header, keyValues);
if (!memKtx) {
qWarning() << " Ktx could not be created, bailing";
@ -1010,9 +1011,9 @@ void NetworkTexture::handleFinishedInitialLoad() {
auto newKtxDescriptor = memKtx->toDescriptor();
texture = gpu::Texture::build(newKtxDescriptor);
texture->setKtxBacking(file);
texture->setSource(filename);
textureAndSize = gpu::Texture::build(newKtxDescriptor);
textureAndSize.first->setKtxBacking(file);
textureAndSize.first->setSource(filename);
auto& images = originalKtxDescriptor->images;
size_t imageSizeRemaining = ktxHighMipData.size();
@ -1025,7 +1026,7 @@ void NetworkTexture::handleFinishedInitialLoad() {
break;
}
ktxData -= image._imageSize;
texture->assignStoredMip(static_cast<gpu::uint16>(level), image._imageSize, ktxData);
textureAndSize.first->assignStoredMip(static_cast<gpu::uint16>(level), image._imageSize, ktxData);
ktxData -= ktx::IMAGE_SIZE_WIDTH;
imageSizeRemaining -= (image._imageSize + ktx::IMAGE_SIZE_WIDTH);
}
@ -1033,13 +1034,13 @@ void NetworkTexture::handleFinishedInitialLoad() {
// We replace the texture with the one stored in the cache. This deals with the possible race condition of two different
// images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will
// be the winner
texture = textureCache->cacheTextureByHash(filename, texture);
textureAndSize = textureCache->cacheTextureByHash(filename, textureAndSize);
}
QMetaObject::invokeMethod(resource.data(), "setImage",
Q_ARG(gpu::TexturePointer, texture),
Q_ARG(int, texture->getWidth()),
Q_ARG(int, texture->getHeight()));
Q_ARG(gpu::TexturePointer, textureAndSize.first),
Q_ARG(int, textureAndSize.second.x),
Q_ARG(int, textureAndSize.second.y));
QMetaObject::invokeMethod(resource.data(), "startRequestForNextMipLevel");
});
@ -1229,15 +1230,15 @@ void ImageReader::read() {
auto textureCache = DependencyManager::get<TextureCache>();
if (textureCache) {
// If we already have a live texture with the same hash, use it
auto texture = textureCache->getTextureByHash(hash);
auto textureAndSize = textureCache->getTextureByHash(hash);
// If there is no live texture, check if there's an existing KTX file
if (!texture) {
if (!textureAndSize.first) {
auto ktxFile = textureCache->_ktxCache->getFile(hash);
if (ktxFile) {
texture = gpu::Texture::unserialize(ktxFile, _url.toString().toStdString());
if (texture) {
texture = textureCache->cacheTextureByHash(hash, texture);
textureAndSize = gpu::Texture::unserialize(ktxFile, _url.toString().toStdString());
if (textureAndSize.first) {
textureAndSize = textureCache->cacheTextureByHash(hash, textureAndSize);
} else {
qCWarning(materialnetworking) << "Invalid cached KTX " << _url << " under hash " << hash.c_str() << ", recreating...";
}
@ -1246,17 +1247,17 @@ void ImageReader::read() {
// If we found the texture either because it's in use or via KTX deserialization,
// set the image and return immediately.
if (texture) {
if (textureAndSize.first) {
QMetaObject::invokeMethod(resource.data(), "setImage",
Q_ARG(gpu::TexturePointer, texture),
Q_ARG(int, texture->getWidth()),
Q_ARG(int, texture->getHeight()));
Q_ARG(gpu::TexturePointer, textureAndSize.first),
Q_ARG(int, textureAndSize.second.x),
Q_ARG(int, textureAndSize.second.y));
return;
}
}
// Proccess new texture
gpu::TexturePointer texture;
std::pair<gpu::TexturePointer, ivec2> textureAndSize;
{
PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0);
@ -1269,23 +1270,23 @@ void ImageReader::read() {
constexpr bool shouldCompress = false;
#endif
auto target = getBackendTarget();
texture = image::processImage(std::move(buffer), _url.toString().toStdString(), _sourceChannel, _maxNumPixels, networkTexture->getTextureType(), shouldCompress, target);
textureAndSize = image::processImage(std::move(buffer), _url.toString().toStdString(), _sourceChannel, _maxNumPixels, networkTexture->getTextureType(), shouldCompress, target);
if (!texture) {
if (!textureAndSize.first) {
QMetaObject::invokeMethod(resource.data(), "setImage",
Q_ARG(gpu::TexturePointer, texture),
Q_ARG(gpu::TexturePointer, textureAndSize.first),
Q_ARG(int, 0),
Q_ARG(int, 0));
return;
}
texture->setSourceHash(hash);
texture->setFallbackTexture(networkTexture->getFallbackTexture());
textureAndSize.first->setSourceHash(hash);
textureAndSize.first->setFallbackTexture(networkTexture->getFallbackTexture());
}
// Save the image into a KTXFile
if (texture && textureCache) {
auto memKtx = gpu::Texture::serialize(*texture);
if (textureAndSize.first && textureCache) {
auto memKtx = gpu::Texture::serialize(*textureAndSize.first, textureAndSize.second);
// Move the texture into a memory mapped file
if (memKtx) {
@ -1294,20 +1295,20 @@ void ImageReader::read() {
auto& ktxCache = textureCache->_ktxCache;
auto file = ktxCache->writeFile(data, KTXCache::Metadata(hash, length));
if (file) {
texture->setKtxBacking(file);
textureAndSize.first->setKtxBacking(file);
}
}
// We replace the texture with the one stored in the cache. This deals with the possible race condition of two different
// images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will
// be the winner
texture = textureCache->cacheTextureByHash(hash, texture);
textureAndSize = textureCache->cacheTextureByHash(hash, textureAndSize);
}
QMetaObject::invokeMethod(resource.data(), "setImage",
Q_ARG(gpu::TexturePointer, texture),
Q_ARG(int, texture->getWidth()),
Q_ARG(int, texture->getHeight()));
Q_ARG(gpu::TexturePointer, textureAndSize.first),
Q_ARG(int, textureAndSize.second.x),
Q_ARG(int, textureAndSize.second.y));
}
NetworkTexturePointer TextureCache::getResourceTexture(const QUrl& resourceTextureUrl) {

View file

@ -183,8 +183,8 @@ public:
const QByteArray& content = QByteArray(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS,
image::ColorChannel sourceChannel = image::ColorChannel::NONE);
gpu::TexturePointer getTextureByHash(const std::string& hash);
gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture);
std::pair<gpu::TexturePointer, glm::ivec2> getTextureByHash(const std::string& hash);
std::pair<gpu::TexturePointer, glm::ivec2> cacheTextureByHash(const std::string& hash, const std::pair<gpu::TexturePointer, glm::ivec2>& textureAndSize);
NetworkTexturePointer getResourceTexture(const QUrl& resourceTextureUrl);
const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height);
@ -226,7 +226,7 @@ private:
std::shared_ptr<cache::FileCache> _ktxCache { std::make_shared<KTXCache>(KTX_DIRNAME, KTX_EXT) };
// Map from image hashes to texture weak pointers
std::unordered_map<std::string, std::weak_ptr<gpu::Texture>> _texturesByHashes;
std::unordered_map<std::string, std::pair<std::weak_ptr<gpu::Texture>, glm::ivec2>> _texturesByHashes;
std::mutex _texturesByHashesMutex;
gpu::TexturePointer _permutationNormalTexture;

View file

@ -415,6 +415,8 @@ void AddressManager::handleLookupString(const QString& lookupString, bool fromSu
QString sanitizedString = lookupString.trimmed();
if (!sanitizedString.isEmpty()) {
resetConfirmConnectWithoutAvatarEntities();
// make this a valid hifi URL and handle it off to handleUrl
handleUrl(sanitizedString, fromSuggestions ? Suggestions : UserInput);
}
@ -874,6 +876,11 @@ bool AddressManager::setDomainInfo(const QUrl& domainURL, LookupTrigger trigger)
return emitHostChanged;
}
void AddressManager::goToEntry(LookupTrigger trigger) {
resetConfirmConnectWithoutAvatarEntities();
handleUrl(DEFAULT_VIRCADIA_ADDRESS, trigger);
}
void AddressManager::goToUser(const QString& username, bool shouldMatchOrientation) {
QString formattedUsername = QUrl::toPercentEncoding(username);
@ -890,6 +897,11 @@ void AddressManager::goToUser(const QString& username, bool shouldMatchOrientati
QByteArray(), nullptr, requestParams);
}
void AddressManager::goToLastAddress() {
resetConfirmConnectWithoutAvatarEntities();
handleUrl(_lastVisitedURL, LookupTrigger::AttemptedRefresh);
}
bool AddressManager::canGoBack() const {
return (_backStack.size() > 0);
}
@ -1024,3 +1036,10 @@ QString AddressManager::getPlaceName() const {
}
return QString();
}
void AddressManager::resetConfirmConnectWithoutAvatarEntities() {
DomainHandler& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
if (!domainHandler.isConnected()) {
domainHandler.resetConfirmConnectWithoutAvatarEntities();
}
}

View file

@ -301,9 +301,7 @@ public slots:
* @param {location.LookupTrigger} trigger=StartupFromSettings - The reason for the function call. Helps ensure that user's
* location history is correctly maintained.
*/
void goToEntry(LookupTrigger trigger = LookupTrigger::StartupFromSettings) {
handleUrl(DEFAULT_VIRCADIA_ADDRESS, trigger);
}
void goToEntry(LookupTrigger trigger = LookupTrigger::StartupFromSettings);
/**jsdoc
* Takes you to the specified user's location.
@ -318,7 +316,7 @@ public slots:
* Takes you to the last address tried. This will be the last URL tried from <code>location.handleLookupString</code>.
* @function location.goToLastAddress
*/
void goToLastAddress() { handleUrl(_lastVisitedURL, LookupTrigger::AttemptedRefresh); }
void goToLastAddress();
/**jsdoc
* Checks if going back to the previous location is possible.
@ -527,6 +525,8 @@ private:
void addCurrentAddressToHistory(LookupTrigger trigger);
void resetConfirmConnectWithoutAvatarEntities();
QUrl _domainURL;
QUrl _lastVisitedURL;

View file

@ -126,6 +126,8 @@ void DomainHandler::hardReset(QString reason) {
emit resetting();
softReset(reason);
_haveAskedConnectWithoutAvatarEntities = false;
_canConnectWithoutAvatarEntities = false;
_isInErrorState = false;
emit redirectErrorStateChanged(_isInErrorState);
@ -364,10 +366,14 @@ void DomainHandler::setIsConnected(bool isConnected) {
_lastDomainConnectionError = -1;
emit connectedToDomain(_domainURL);
// FIXME: Reinstate the requestDomainSettings() call here in version 2021.2.0 instead of having it in
// NodeList::processDomainServerList().
/*
if (_domainURL.scheme() == URL_SCHEME_HIFI && !_domainURL.host().isEmpty()) {
// we've connected to new domain - time to ask it for global settings
requestDomainSettings();
}
*/
} else {
emit disconnectedFromDomain();
@ -375,6 +381,24 @@ void DomainHandler::setIsConnected(bool isConnected) {
}
}
void DomainHandler::setCanConnectWithoutAvatarEntities(bool canConnect) {
_canConnectWithoutAvatarEntities = canConnect;
_haveAskedConnectWithoutAvatarEntities = true;
}
bool DomainHandler::canConnectWithoutAvatarEntities() {
if (!_canConnectWithoutAvatarEntities && !_haveAskedConnectWithoutAvatarEntities) {
if (_isConnected) {
// Already connected so don't ask. (Permission removed from user while in the domain.)
setCanConnectWithoutAvatarEntities(true);
} else {
// Ask whether to connect to the domain.
emit confirmConnectWithoutAvatarEntities();
}
}
return _canConnectWithoutAvatarEntities;
}
void DomainHandler::connectedToServerless(std::map<QString, QString> namedPaths) {
_namedPaths = namedPaths;
setIsConnected(true);

View file

@ -98,6 +98,7 @@ public:
Node::LocalID getLocalID() const { return _localID; }
void setLocalID(Node::LocalID localID) { _localID = localID; }
QString getScheme() const { return _domainURL.scheme(); }
QString getHostname() const { return _domainURL.host(); }
QUrl getErrorDomainURL(){ return _errorDomainURL; }
@ -133,6 +134,9 @@ public:
bool isConnected() const { return _isConnected; }
void setIsConnected(bool isConnected);
void setCanConnectWithoutAvatarEntities(bool canConnect);
bool canConnectWithoutAvatarEntities();
bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; }
bool getInterstitialModeEnabled() const;
void setInterstitialModeEnabled(bool enableInterstitialMode);
@ -159,6 +163,10 @@ public:
bool checkInPacketTimeout();
void clearPendingCheckins() { _checkInPacketsSinceLastReply = 0; }
void resetConfirmConnectWithoutAvatarEntities() {
_haveAskedConnectWithoutAvatarEntities = false;
}
/**jsdoc
* <p>The reasons that you may be refused connection to a domain are defined by numeric values:</p>
* <table>
@ -252,6 +260,7 @@ signals:
void completedSocketDiscovery();
void resetting();
void confirmConnectWithoutAvatarEntities();
void connectedToDomain(QUrl domainURL);
void disconnectedFromDomain();
@ -287,6 +296,8 @@ private:
HifiSockAddr _iceServerSockAddr;
NetworkPeer _icePeer;
bool _isConnected { false };
bool _haveAskedConnectWithoutAvatarEntities { false };
bool _canConnectWithoutAvatarEntities { false };
bool _isInErrorState { false };
QJsonObject _settingsObject;
QString _pendingPath;

View file

@ -198,6 +198,10 @@ void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) {
newPermissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData)) {
emit canGetAndSetPrivateUserDataChanged(_permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData));
}
if (originalPermissions.can(NodePermissions::Permission::canRezAvatarEntities) !=
newPermissions.can(NodePermissions::Permission::canRezAvatarEntities)) {
emit canRezAvatarEntitiesChanged(_permissions.can(NodePermissions::Permission::canRezAvatarEntities));
}
}
void LimitedNodeList::setSocketLocalPort(quint16 socketLocalPort) {

View file

@ -133,6 +133,7 @@ public:
bool getThisNodeCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); }
bool getThisNodeCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); }
bool getThisNodeCanGetAndSetPrivateUserData() const { return _permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData); }
bool getThisNodeCanRezAvatarEntities() const { return _permissions.can(NodePermissions::Permission::canRezAvatarEntities); }
quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); }
Q_INVOKABLE void setSocketLocalPort(quint16 socketLocalPort);
@ -390,6 +391,7 @@ signals:
void canKickChanged(bool canKick);
void canReplaceContentChanged(bool canReplaceContent);
void canGetAndSetPrivateUserDataChanged(bool canGetAndSetPrivateUserData);
void canRezAvatarEntitiesChanged(bool canRezAvatarEntities);
protected slots:
void connectedForLocalSocketTest();

View file

@ -84,6 +84,7 @@ public:
bool getCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); }
bool getCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); }
bool getCanGetAndSetPrivateUserData() const { return _permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData); }
bool getCanRezAvatarEntities() const { return _permissions.can(NodePermissions::Permission::canRezAvatarEntities); }
using NodesIgnoredPair = std::pair<std::vector<QUuid>, bool>;

View file

@ -42,6 +42,7 @@
#include "udt/PacketHeaders.h"
#include "SharedUtil.h"
#include <Trace.h>
#include <ModerationFlags.h>
using namespace std::chrono;
@ -94,6 +95,12 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
// send a ping punch immediately
connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer);
// FIXME: Can remove this temporary work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.)
// Adjust our canRezAvatarEntities permissions on older domains that do not have this setting.
// DomainServerList and DomainSettings packets can come in either order so need to adjust with both occurrences.
auto nodeList = DependencyManager::get<NodeList>();
connect(&_domainHandler, &DomainHandler::settingsReceived, this, &NodeList::adjustCanRezAvatarEntitiesPerSettings);
auto accountManager = DependencyManager::get<AccountManager>();
// assume that we may need to send a new DS check in anytime a new keypair is generated
@ -726,6 +733,11 @@ void NodeList::processDomainServerList(QSharedPointer<ReceivedMessage> message)
// pull the permissions/right/privileges for this node out of the stream
NodePermissions newPermissions;
packetStream >> newPermissions;
// FIXME: Can remove this temporary work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.)
// Adjust our canRezAvatarEntities permissions on older domains that do not have this setting.
// DomainServerList and DomainSettings packets can come in either order so need to adjust with both occurrences.
bool adjustedPermissions = adjustCanRezAvatarEntitiesPermissions(_domainHandler.getSettingsObject(), newPermissions, false);
// Is packet authentication enabled?
bool isAuthenticated;
packetStream >> isAuthenticated;
@ -781,7 +793,7 @@ void NodeList::processDomainServerList(QSharedPointer<ReceivedMessage> message)
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList);
if (_domainHandler.isConnected() && _domainHandler.getUUID() != domainUUID) {
// Recieved packet from different domain.
// Received packet from different domain.
qWarning() << "IGNORING DomainList packet from" << domainUUID << "while connected to"
<< _domainHandler.getUUID() << ": sent " << pingLagTime << " msec ago.";
qWarning(networking) << "DomainList request lag (interface->ds): " << domainServerRequestLag << "msec";
@ -809,6 +821,23 @@ void NodeList::processDomainServerList(QSharedPointer<ReceivedMessage> message)
setSessionLocalID(newLocalID);
setSessionUUID(newUUID);
// FIXME: Remove this call to requestDomainSettings() and reinstate the one in DomainHandler::setIsConnected(), in version
// 2021.2.0. (New protocol version implies a domain server upgrade.)
if (!_domainHandler.isConnected()
&& _domainHandler.getScheme() == URL_SCHEME_HIFI && !_domainHandler.getHostname().isEmpty()) {
// We're about to connect but we need the domain settings (in particular, the node permissions) in order to adjust the
// canRezAvatarEntities permission above before using the permissions in determining whether or not to connect without
// avatar entities rezzing below.
_domainHandler.requestDomainSettings();
}
// Don't continue login to the domain if have avatar entities and don't have permissions to rez them, unless user has OKed
// continuing login.
if (!newPermissions.can(NodePermissions::Permission::canRezAvatarEntities)
&& (!adjustedPermissions || !_domainHandler.canConnectWithoutAvatarEntities())) {
return;
}
// if this was the first domain-server list from this domain, we've now connected
if (!_domainHandler.isConnected()) {
_domainHandler.setLocalID(domainLocalID);
@ -1263,17 +1292,19 @@ float NodeList::getInjectorGain() {
return _injectorGain;
}
void NodeList::kickNodeBySessionID(const QUuid& nodeID) {
void NodeList::kickNodeBySessionID(const QUuid& nodeID, unsigned int banFlags) {
// 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() && getSessionUUID() != nodeID ) {
if (getThisNodeCanKick()) {
// setup the packet
auto kickPacket = NLPacket::create(PacketType::NodeKickRequest, NUM_BYTES_RFC4122_UUID, true);
auto kickPacket = NLPacket::create(PacketType::NodeKickRequest, NUM_BYTES_RFC4122_UUID + sizeof(int), true);
// write the node ID to the packet
kickPacket->write(nodeID.toRfc4122());
// write the ban parameters to the packet
kickPacket->writePrimitive(banFlags);
qCDebug(networking) << "Sending packet to kick node" << uuidStringWithoutCurlyBraces(nodeID);
@ -1368,3 +1399,35 @@ void NodeList::setRequestsDomainListData(bool isRequesting) {
void NodeList::startThread() {
moveToNewNamedThread(this, "NodeList Thread", QThread::TimeCriticalPriority);
}
// FIXME: Can remove this work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.)
bool NodeList::adjustCanRezAvatarEntitiesPermissions(const QJsonObject& domainSettingsObject,
NodePermissions& permissions, bool notify) {
if (domainSettingsObject.isEmpty()) {
// Don't have enough information to adjust yet.
return false; // Failed to adjust.
}
const double CANREZAVATARENTITIES_INTRODUCED_VERSION = 2.5;
auto version = domainSettingsObject.value("version");
if (version.isUndefined() || version.isDouble() && version.toDouble() < CANREZAVATARENTITIES_INTRODUCED_VERSION) {
// On domains without the canRezAvatarEntities permission available, set it to the same as canConnectToDomain.
if (permissions.can(NodePermissions::Permission::canConnectToDomain)) {
if (!permissions.can(NodePermissions::Permission::canRezAvatarEntities)) {
permissions.set(NodePermissions::Permission::canRezAvatarEntities);
if (notify) {
emit canRezAvatarEntitiesChanged(permissions.can(NodePermissions::Permission::canRezAvatarEntities));
}
}
}
}
return true; // Successfully adjusted.
}
// FIXME: Can remove this work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.)
void NodeList::adjustCanRezAvatarEntitiesPerSettings(const QJsonObject& domainSettingsObject) {
adjustCanRezAvatarEntitiesPermissions(domainSettingsObject, _permissions, true);
}

View file

@ -86,7 +86,7 @@ public:
void setInjectorGain(float gain);
float getInjectorGain();
void kickNodeBySessionID(const QUuid& nodeID);
void kickNodeBySessionID(const QUuid& nodeID, unsigned int banFlags);
void muteNodeBySessionID(const QUuid& nodeID);
void requestUsernameFromSessionID(const QUuid& nodeID);
bool getRequestsDomainListData() { return _requestsDomainListData; }
@ -123,6 +123,11 @@ public slots:
void processUsernameFromIDReply(QSharedPointer<ReceivedMessage> message);
// FIXME: Can remove these work-arounds in version 2021.2.0. (New protocol version implies a domain server upgrade.)
bool adjustCanRezAvatarEntitiesPermissions(const QJsonObject& domainSettingsObject, NodePermissions& permissions,
bool notify);
void adjustCanRezAvatarEntitiesPerSettings(const QJsonObject& domainSettingsObject);
#if (PR_BUILD || DEV_BUILD)
void toggleSendNewerDSConnectVersion(bool shouldSendNewerVersion) { _shouldSendNewerVersion = shouldSendNewerVersion; }
#endif

View file

@ -57,6 +57,7 @@ NodePermissions::NodePermissions(QMap<QString, QVariant> perms) {
permissions = NodePermissions::Permissions();
permissions |= perms["id_can_connect"].toBool() ? Permission::canConnectToDomain : Permission::none;
permissions |= perms["id_can_rez_avatar_entities"].toBool() ? Permission::canRezAvatarEntities : Permission::none;
permissions |= perms["id_can_adjust_locks"].toBool() ? Permission::canAdjustLocks : Permission::none;
permissions |= perms["id_can_rez"].toBool() ? Permission::canRezPermanentEntities : Permission::none;
permissions |= perms["id_can_rez_tmp"].toBool() ? Permission::canRezTemporaryEntities : Permission::none;
@ -67,7 +68,8 @@ NodePermissions::NodePermissions(QMap<QString, QVariant> perms) {
Permission::canConnectPastMaxCapacity : Permission::none;
permissions |= perms["id_can_kick"].toBool() ? Permission::canKick : Permission::none;
permissions |= perms["id_can_replace_content"].toBool() ? Permission::canReplaceDomainContent : Permission::none;
permissions |= perms["id_can_get_and_set_private_user_data"].toBool() ? Permission::canGetAndSetPrivateUserData : Permission::none;
permissions |= perms["id_can_get_and_set_private_user_data"].toBool() ?
Permission::canGetAndSetPrivateUserData : Permission::none;
}
QVariant NodePermissions::toVariant(QHash<QUuid, GroupRank> groupRanks) {
@ -86,6 +88,7 @@ QVariant NodePermissions::toVariant(QHash<QUuid, GroupRank> groupRanks) {
}
}
values["id_can_connect"] = can(Permission::canConnectToDomain);
values["id_can_rez_avatar_entities"] = can(Permission::canRezAvatarEntities);
values["id_can_adjust_locks"] = can(Permission::canAdjustLocks);
values["id_can_rez"] = can(Permission::canRezPermanentEntities);
values["id_can_rez_tmp"] = can(Permission::canRezTemporaryEntities);
@ -141,6 +144,9 @@ QDebug operator<<(QDebug debug, const NodePermissions& perms) {
if (perms.can(NodePermissions::Permission::canConnectToDomain)) {
debug << " connect";
}
if (perms.can(NodePermissions::Permission::canRezAvatarEntities)) {
debug << " rez-avatar-entities";
}
if (perms.can(NodePermissions::Permission::canAdjustLocks)) {
debug << " locks";
}
@ -174,6 +180,7 @@ QDebug operator<<(QDebug debug, const NodePermissions& perms) {
debug.nospace() << "]";
return debug.nospace();
}
QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms) {
if (perms) {
return operator<<(debug, *perms.get());

View file

@ -80,7 +80,8 @@ public:
canReplaceDomainContent = 128,
canRezPermanentCertifiedEntities = 256,
canRezTemporaryCertifiedEntities = 512,
canGetAndSetPrivateUserData = 1024
canGetAndSetPrivateUserData = 1024,
canRezAvatarEntities = 2048
};
Q_DECLARE_FLAGS(Permissions, Permission)
Permissions permissions;

View file

@ -85,9 +85,9 @@ private:
class UnsourcedListenerReference : public ListenerReference {
public:
inline UnsourcedListenerReference(T* target, void (T::*slot)(QSharedPointer<ReceivedMessage>));
virtual bool invokeDirectly(const QSharedPointer<ReceivedMessage>& receivedMessagePointer, const QSharedPointer<Node>& sourceNode);
virtual bool isSourced() const { return false; }
virtual QObject* getObject() const { return _target; }
virtual bool invokeDirectly(const QSharedPointer<ReceivedMessage>& receivedMessagePointer, const QSharedPointer<Node>& sourceNode) override;
virtual bool isSourced() const override { return false; }
virtual QObject* getObject() const override { return _target; }
private:
QPointer<T> _target;
@ -98,9 +98,9 @@ private:
class SourcedListenerReference : public ListenerReference {
public:
inline SourcedListenerReference(T* target, void (T::*slot)(QSharedPointer<ReceivedMessage>, QSharedPointer<Node>));
virtual bool invokeDirectly(const QSharedPointer<ReceivedMessage>& receivedMessagePointer, const QSharedPointer<Node>& sourceNode);
virtual bool isSourced() const { return true; }
virtual QObject* getObject() const { return _target; }
virtual bool invokeDirectly(const QSharedPointer<ReceivedMessage>& receivedMessagePointer, const QSharedPointer<Node>& sourceNode) override;
virtual bool isSourced() const override { return true; }
virtual QObject* getObject() const override { return _target; }
private:
QPointer<T> _target;

View file

@ -4,6 +4,7 @@
//
// Created by Ryan Huffman on 6/06/16.
// Copyright 2016 High Fidelity, Inc.
// Copyright 2021 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html

View file

@ -696,10 +696,29 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
return;
}
// rotate into motor-frame
const bool motorHasRotation = !(motor.rotation == btQuaternion::getIdentity());
btVector3 axis = motor.rotation.getAxis();
btScalar angle = motor.rotation.getAngle();
btVector3 velocity = worldVelocity.rotate(axis, -angle);
// Rotate a vector from motor frame to world frame
auto rotateToWorldFrame = [&axis, &angle, &motorHasRotation](const btVector3 vectorInMotorFrame) {
if (motorHasRotation) {
return vectorInMotorFrame.rotate(axis, angle);
} else {
return vectorInMotorFrame;
}
};
// Rotate a vector from world frame to motor frame
auto rotateToMotorFrame = [&axis, &angle, &motorHasRotation](const btVector3 vectorInWorldFrame) {
if (motorHasRotation) {
return vectorInWorldFrame.rotate(axis, -angle);
} else {
return vectorInWorldFrame;
}
};
btVector3 velocity = rotateToMotorFrame(worldVelocity);
int32_t collisionMask = computeCollisionMask();
if (collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS ||
@ -712,15 +731,15 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
velocity += tau * (motor.velocity - velocity);
// rotate back into world-frame
velocity = velocity.rotate(axis, angle);
_targetVelocity += (tau * motor.velocity).rotate(axis, angle);
velocity = rotateToWorldFrame(velocity);
_targetVelocity += rotateToWorldFrame(tau * motor.velocity);
// store the velocity and weight
velocities.push_back(velocity);
weights.push_back(tau);
} else {
// compute local UP
btVector3 up = _currentUp.rotate(axis, -angle);
btVector3 up = rotateToMotorFrame(_currentUp);
btVector3 motorVelocity = motor.velocity;
// save these non-adjusted components for later
@ -729,7 +748,7 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
if (_stepHeight > _minStepHeight && !_steppingUp) {
// there is a step --> compute velocity direction to go over step
btVector3 motorVelocityWF = motorVelocity.rotate(axis, angle);
btVector3 motorVelocityWF = rotateToWorldFrame(motorVelocity);
if (motorVelocityWF.dot(_stepNormal) < 0.0f) {
// the motor pushes against step
_steppingUp = true;
@ -764,8 +783,8 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
}
// add components back together and rotate into world-frame
velocity = (hVelocity + vVelocity).rotate(axis, angle);
_targetVelocity += maxTau * (hTargetVelocity + vTargetVelocity).rotate(axis, angle);
velocity = rotateToWorldFrame(hVelocity + vVelocity);
_targetVelocity += maxTau * rotateToWorldFrame(hTargetVelocity + vTargetVelocity);
// store velocity and weights
velocities.push_back(velocity);

View file

@ -821,14 +821,12 @@ void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape) {
void GeometryCache::renderShape(gpu::Batch& batch, Shape shape, const glm::vec4& color) {
batch.setInputFormat(getSolidStreamFormat());
// Color must be set after input format
batch._glColor4f(color.r, color.g, color.b, color.a);
_shapes[shape].draw(batch);
}
void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape, const glm::vec4& color) {
batch.setInputFormat(getWireStreamFormat());
// Color must be set after input format
batch._glColor4f(color.r, color.g, color.b, color.a);
_shapes[shape].drawWire(batch);
}

View file

@ -92,7 +92,7 @@ private:
bool _cauterized { false };
bool _cullWithParent { false };
QVector<QUuid> _renderWithZones;
BillboardMode _billboardMode;
BillboardMode _billboardMode { BillboardMode::NONE };
uint64_t _created;
Transform _localTransform;

View file

@ -35,6 +35,7 @@ public:
void setParent(TreeNodeFolder* parent) { _parent = parent; }
TreeNodeType getType() { return _type; }
const QString& getName() { return _name; };
virtual ~TreeNodeBase() = default;
private:
TreeNodeFolder* _parent;

View file

@ -4,6 +4,7 @@
//
// Created by Stephen Birarda on 2016-07-11.
// Copyright 2016 High Fidelity, Inc.
// Copyright 2021 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@ -51,15 +52,14 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) {
return DependencyManager::get<NodeList>()->getAvatarGain(nodeID);
}
void UsersScriptingInterface::kick(const QUuid& nodeID) {
void UsersScriptingInterface::kick(const QUuid& nodeID, unsigned int banFlags) {
if (_kickConfirmationOperator) {
bool waitingForKickResponse = _kickResponseLock.resultWithReadLock<bool>([&] { return _waitingForKickResponse; });
if (getCanKick() && !waitingForKickResponse) {
_kickConfirmationOperator(nodeID);
_kickConfirmationOperator(nodeID, banFlags);
}
} else {
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID);
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID, banFlags);
}
}

View file

@ -4,6 +4,7 @@
//
// Created by Stephen Birarda on 2016-07-11.
// Copyright 2016 High Fidelity, Inc.
// Copyright 2021 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@ -16,6 +17,7 @@
#include <DependencyManager.h>
#include <shared/ReadWriteLockable.h>
#include <ModerationFlags.h>
/**jsdoc
* The <code>Users</code> API provides features to regulate your interaction with other users.
@ -31,6 +33,10 @@
* <code>false</code>. <em>Read-only.</em>
* @property {boolean} requestsDomainListData - <code>true</code> if the client requests extra data from the mixers (such as
* positional data of an avatar they've ignored). <em>Read-only.</em>
* @property {BanFlags} NO_BAN - Do not ban user. <em>Read-only.</em>
* @property {BanFlags} BAN_BY_USERNAME - Ban user by username. <em>Read-only.</em>
* @property {BanFlags} BAN_BY_FINGERPRINT - Ban user by fingerprint. <em>Read-only.</em>
* @property {BanFlags} BAN_BY_IP - Ban user by IP address. <em>Read-only.</em>
*/
class UsersScriptingInterface : public QObject, public Dependency {
Q_OBJECT
@ -39,9 +45,14 @@ class UsersScriptingInterface : public QObject, public Dependency {
Q_PROPERTY(bool canKick READ getCanKick)
Q_PROPERTY(bool requestsDomainListData READ getRequestsDomainListData WRITE setRequestsDomainListData)
Q_PROPERTY(unsigned int NO_BAN READ getNoBan CONSTANT)
Q_PROPERTY(unsigned int BAN_BY_USERNAME READ getBanByUsername CONSTANT)
Q_PROPERTY(unsigned int BAN_BY_FINGERPRINT READ getBanByFingerprint CONSTANT)
Q_PROPERTY(unsigned int BAN_BY_IP READ getBanByIP CONSTANT)
public:
UsersScriptingInterface();
void setKickConfirmationOperator(std::function<void(const QUuid& nodeID)> kickConfirmationOperator) {
void setKickConfirmationOperator(std::function<void(const QUuid& nodeID, unsigned int banFlags)> kickConfirmationOperator) {
_kickConfirmationOperator = kickConfirmationOperator;
}
@ -111,13 +122,14 @@ public slots:
float getAvatarGain(const QUuid& nodeID);
/**jsdoc
* Kicks and bans a user. This removes them from the server and prevents them from returning. The ban is by user name if
* available, or machine fingerprint otherwise.
* Kicks and bans a user. This removes them from the server and prevents them from returning. The ban is by user name (if
* available) and by machine fingerprint. The ban functionality can be controlled with flags.
* <p>This function only works if you're an administrator of the domain you're in.</p>
* @function Users.kick
* @param {Uuid} sessionID - The session ID of the user to kick and ban.
* @param {BanFlags} - Preferred ban flags. <i>Bans a user by username (if available) and machine fingerprint by default.</i>
*/
void kick(const QUuid& nodeID);
void kick(const QUuid& nodeID, unsigned int banFlags = ModerationFlags::getDefaultBanFlags());
/**jsdoc
* Mutes a user's microphone for everyone. The mute is not permanent: the user can unmute themselves.
@ -237,7 +249,12 @@ private:
bool getRequestsDomainListData();
void setRequestsDomainListData(bool requests);
std::function<void(const QUuid& nodeID)> _kickConfirmationOperator;
static constexpr unsigned int getNoBan() { return ModerationFlags::BanFlags::NO_BAN; };
static constexpr unsigned int getBanByUsername() { return ModerationFlags::BanFlags::BAN_BY_USERNAME; };
static constexpr unsigned int getBanByFingerprint() { return ModerationFlags::BanFlags::BAN_BY_FINGERPRINT; };
static constexpr unsigned int getBanByIP() { return ModerationFlags::BanFlags::BAN_BY_IP; };
std::function<void(const QUuid& nodeID, unsigned int banFlags)> _kickConfirmationOperator;
ReadWriteLockable _kickResponseLock;
bool _waitingForKickResponse { false };

View file

@ -0,0 +1,45 @@
//
// ModerationFlags.h
// libraries/shared/src
//
// Created by Kalila L. on Mar 11 2021.
// Copyright 2021 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef vircadia_ModerationFlags_h
#define vircadia_ModerationFlags_h
class ModerationFlags {
public:
/**jsdoc
* <p>A set of flags for moderation ban actions. The value is constructed by using the <code>|</code> (bitwise OR) operator on the
* individual flag values.</p>
* <table>
* <thead>
* <tr><th>Flag Name</th><th>Value</th><th>Description</th></tr>
* </thead>
* <tbody>
* <tr><td>NO_BAN</td><td><code>0</code></td><td>Don't ban user when kicking. <em>This does not currently have an effect.</em></td></tr>
* <tr><td>BAN_BY_USERNAME</td><td><code>1</code></td><td>Ban the person by their username.</td></tr>
* <tr><td>BAN_BY_FINGERPRINT</td><td><code>2</code></td><td>Ban the person by their machine fingerprint.</td></tr>
* <tr><td>BAN_BY_IP</td><td><code>4</code></td><td>Ban the person by their IP address.</td></tr>
* </tbody>
* </table>
* @typedef {number} BanFlags
*/
enum BanFlags
{
NO_BAN = 0,
BAN_BY_USERNAME = 1,
BAN_BY_FINGERPRINT = 2,
BAN_BY_IP = 4
};
static constexpr unsigned int getDefaultBanFlags() { return (BanFlags::BAN_BY_USERNAME | BanFlags::BAN_BY_FINGERPRINT); };
};
#endif // vircadia_ModerationFlags_h

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View file

@ -35,7 +35,8 @@ Script.include([
"../libraries/gridTool.js",
"entityList/entityList.js",
"entitySelectionTool/entitySelectionTool.js",
"audioFeedback/audioFeedback.js"
"audioFeedback/audioFeedback.js",
"modules/brokenURLReport.js"
]);
var CreateWindow = Script.require('./modules/createWindow.js');

View file

@ -413,12 +413,14 @@ EntityListTool = function(shouldUseEditTabletApp) {
alignGridToSelection();
} else if (data.type === 'alignGridToAvatar') {
alignGridToAvatar();
} else if (data.type === 'brokenURLReport') {
brokenURLReport(selectionManager.selections);
} else if (data.type === 'toggleGridVisibility') {
toggleGridVisibility();
} else if (data.type === 'toggleSnapToGrid') {
that.toggleSnapToGrid();
that.toggleSnapToGrid();
}
};
webView.webEventReceived.connect(onWebEventReceived);

View file

@ -308,7 +308,14 @@
<div class = "menu-item-caption">Align Grid to Avatar</div>
<div class = "menu-item-shortcut">K</div>
</div>
</button>
</button>
<div class="menu-separator"></div>
<button class="menu-button" id="brokenURLReport" >
<div class = "menu-item">
<div class = "menu-item-caption">Generate Broken URL Report</div>
<div class = "menu-item-shortcut"></div>
</div>
</button>
</div>
<div id="menuBackgroundOverlay" ></div>
</body>

View file

@ -273,6 +273,7 @@ let elEntityTable,
elSnapToGridActivatorCaption,
elAlignGridToSelection,
elAlignGridToAvatar,
elBrokenURLReport,
elFilterTypeMultiselectBox,
elFilterTypeText,
elFilterTypeOptions,
@ -359,7 +360,8 @@ function loaded() {
elSnapToGridActivator = document.getElementById("snapToGridActivator");
elSnapToGridActivatorCaption = document.getElementById("snapToGridActivatorCaption");
elAlignGridToSelection = document.getElementById("alignGridToSelection");
elAlignGridToAvatar = document.getElementById("alignGridToAvatar");
elAlignGridToAvatar = document.getElementById("alignGridToAvatar");
elBrokenURLReport = document.getElementById("brokenURLReport");
elFilterTypeMultiselectBox = document.getElementById("filter-type-multiselect-box");
elFilterTypeText = document.getElementById("filter-type-text");
elFilterTypeOptions = document.getElementById("filter-type-options");
@ -597,7 +599,11 @@ function loaded() {
elAlignGridToAvatar.onclick = function () {
EventBridge.emitWebEvent(JSON.stringify({ type: "alignGridToAvatar" }));
closeAllEntityListMenu();
};
};
elBrokenURLReport.onclick = function () {
EventBridge.emitWebEvent(JSON.stringify({ type: "brokenURLReport" }));
closeAllEntityListMenu();
};
elToggleSpaceMode.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: "toggleSpaceMode" }));
};

View file

@ -0,0 +1,102 @@
<!DOCTYPE html>
<!--//
// brokenURLReport.html
//
// Created by Alezia Kurdis on February 22, 2021.
// Copyright 2021 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//-->
<html>
<head>
<meta charset="utf-8" />
<style>
body {
background-color:#000000;
color:#ffffff;
font-family: Helvetica, Arial, Sans-Serif;
font-weight: 700;
font-size: 12px;
text-align: center;
width: 100%;
}
h1 {
font-size: 18px;
font-weight: 900;
}
table {
text-align: left;
width:96%;
border: 1px solid #DDDDDD;
border-collapse: collapse;
}
td {
border: 1px solid #DDDDDD;
padding: 3px;
}
td.superheader {
font-size: 18px;
text-align: center;
font-weight: 900;
background-color:#999999;
}
td.header {
font-weight: 900;
background-color:#777777;
}
a {
font-size: 22px;
font-weight: 500;
}
a:link {
color: #00b3ff;
background-color: transparent;
text-decoration: none;
}
a:visited {
color: #00b3ff;
background-color: transparent;
text-decoration: none;
}
a:hover {
color: #99e1ff;
background-color: transparent;
text-decoration: none;
}
a:active {
color: #99e1ff;
background-color: transparent;
text-decoration: none;
}
</style>
</head>
<body>
<div id="report">
<div style='text-align: center; width: 100%;'>
<h1>
<br>
<br>
Testing in progress...
</h1>
<br>
<img src='../assets/images/processing.gif'>
</div>
</div>
</body>
<script>
EventBridge.scriptEventReceived.connect(function (message) {
document.getElementById("report").innerHTML = message;
});
function selectEntity(id) {
EventBridge.emitWebEvent(JSON.stringify({
"action": "select",
"entityID": id
}));
}
</script>
</html>

View file

@ -0,0 +1,393 @@
//
// brokenURLReport.js
//
// Created by Alezia Kurdis on February 22, 2021.
// Copyright 2021 Vircadia contributors.
//
// This script reports broken URLs to the Create Application.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var brokenURLReportHttpRequest;
var brokenURLReportUrlList = [];
var brokenURLReportInvalideUrlList = [];
var brokenURLReportProcessedUrlNo = 0;
var brokenURLReportUrlEntry;
var brokenURLReportMessageBox;
var brokenURLReportOverlayWebWindow;
var BROKEN_URL_REPORT_YES_BUTTON = 0x4000;
var BROKEN_URL_REPORT_NO_BUTTON = 0x10000;
var MAX_URL_BEFORE_WARNING_FOR_LONG_PROCESS = 20;
function brokenURLReportRequestUrlValidityCheck(no) {
brokenURLReportHttpRequest = new XMLHttpRequest();
brokenURLReportHttpRequest.requestComplete.connect(brokenURLReportGetResponseStatus);
brokenURLReportHttpRequest.open("GET", brokenURLReportUrlList[no].url);
brokenURLReportHttpRequest.send();
}
function brokenURLReportGetResponseStatus() {
if (brokenURLReportHttpRequest.status === 0 || brokenURLReportHttpRequest.status > 299) {
if (brokenURLReportHttpRequest.status === 0) {
brokenURLReportUrlList[brokenURLReportProcessedUrlNo].validity = "0 - URL not well-formed";
} else {
brokenURLReportUrlList[brokenURLReportProcessedUrlNo].validity = brokenURLReportHttpRequest.status + " - " + brokenURLReportHttpRequest.statusText;
}
brokenURLReportInvalideUrlList.push(brokenURLReportUrlList[brokenURLReportProcessedUrlNo]);
}
brokenURLReportHttpRequest.requestComplete.disconnect(brokenURLReportGetResponseStatus);
brokenURLReportHttpRequest = null;
brokenURLReportProcessedUrlNo = brokenURLReportProcessedUrlNo + 1;
if (brokenURLReportProcessedUrlNo === brokenURLReportUrlList.length) {
brokenURLReportGenerateFormatedReport(brokenURLReportInvalideUrlList);
brokenURLReportUrlList = [];
brokenURLReportInvalideUrlList = [];
brokenURLReportProcessedUrlNo = 0;
} else {
brokenURLReportRequestUrlValidityCheck(brokenURLReportProcessedUrlNo);
}
}
function brokenURLReportGenerateFormatedReport(brokenURLReportInvalideUrlList) {
var brokenURLReportContent = "";
if (brokenURLReportInvalideUrlList.length === 0) {
brokenURLReportContent = "<h1>Broken URL Report<br><br>" + brokenURLReportUrlList.length + " URL tested.<br><font style='color:#00FF00;'>NO ISSUES HAVE BEEN FOUND.</font></h1><br><br><br>";
brokenURLReportContent += "<div style='width: 100%; text-align: left;'><hr><font style='font-size: 11px; font-weight: 500;'><i>This report ignores Asset Server URLs (atp://), local drive paths, and any string not starting with 'http'.</i></font></div>";
Script.setTimeout(function () {
brokenURLReportOverlayWebWindow.emitScriptEvent(brokenURLReportContent);
}, 3000);
return;
}
brokenURLReportContent = " <h1>Broken URL Report</h1>\n";
brokenURLReportContent += " <table>\n";
brokenURLReportContent += " <tr>\n";
brokenURLReportContent += " <td class='superheader'>&nbsp;</td>\n";
brokenURLReportContent += " <td class='superheader' colspan='3'>Entity</td>\n";
brokenURLReportContent += " <td class='superheader' colspan='3'>Broken Url</td>\n";
brokenURLReportContent += " </tr>\n";
brokenURLReportContent += " <tr>\n";
brokenURLReportContent += " <td class='header'>No</td>\n";
brokenURLReportContent += " <td class='header'>Type</td>\n";
brokenURLReportContent += " <td class='header'>Name &amp; ID</td>\n";
brokenURLReportContent += " <td class='header'>&nbsp;</td>\n";
brokenURLReportContent += " <td class='header'>Property</td>\n";
brokenURLReportContent += " <td class='header'>Status</td>\n";
brokenURLReportContent += " <td class='header'>Current URL</td>\n";
brokenURLReportContent += " </tr>\n";
for (var i = 0; i < brokenURLReportInvalideUrlList.length; i++ ){
brokenURLReportContent += " <tr>\n";
brokenURLReportContent += " <td style='color: #999999; width: 5%;'>" + (i + 1) + "</td>\n";
brokenURLReportContent += " <td style='width: 10%;'>" + brokenURLReportInvalideUrlList[i].type + "</td>\n";
brokenURLReportContent += " <td style='width: 40%;'>" + brokenURLReportInvalideUrlList[i].name + "<br><font color='#999999'>" + brokenURLReportInvalideUrlList[i].id + "</font></td>\n";
brokenURLReportContent += " <td style='width: 2%;'><a href='' onclick='selectEntity(" + '"' + brokenURLReportInvalideUrlList[i].id + '"' + "); return false;'>&#9998;</a></td>\n";
brokenURLReportContent += " <td style='color: " + brokenURLReportGetUrlTypeColor(brokenURLReportInvalideUrlList[i].urlType) + "; width: 10%;'>" + brokenURLReportInvalideUrlList[i].urlType + "</td>\n";
brokenURLReportContent += " <td style='background-color: #FF0000; color: #FFFFFF; width: 8%;'>" + brokenURLReportInvalideUrlList[i].validity + "</td>\n";
brokenURLReportContent += " <td style='word-wrap: break-word; width:200px;'>" + brokenURLReportInvalideUrlList[i].url + "</td>\n";
brokenURLReportContent += " </tr>\n";
}
brokenURLReportContent += " </table>\n";
brokenURLReportContent += "<div style='width: 100%; text-align: left;'><br>" + brokenURLReportUrlList.length + " URL tested.<br><br>";
brokenURLReportContent += "<hr><font style='font-size: 11px; font-weight: 500;'><i>This report ignores Asset Server URLs (atp://), local drive paths, and any string not starting with 'http'.</i></font></div>";
Script.setTimeout(function () {
brokenURLReportOverlayWebWindow.emitScriptEvent(brokenURLReportContent);
}, 3000);
}
function brokenURLReportGetUrlTypeColor(urlType) {
var color = "#FFFFFF";
switch (urlType) {
case "script":
color = "#00FF00";
break;
case "serverScripts":
color = "#FF00FF";
break;
case "imageURL":
color = "#00FFFF";
break;
case "materialURL":
color = "#FF6600";
break;
case "modelURL":
color = "#FFFF00";
break;
case "compoundShapeURL":
color = "#6666FF";
break;
case "animation.url":
color = "#6699FF";
break;
case "textures":
color = "#FF0066";
break;
case "xTextureURL":
color = "#0000FF";
break;
case "yTextureURL":
color = "#009966";
break;
case "zTextureURL":
color = "#993366";
break;
case "font":
color = "#FFFFFF";
break;
case "sourceUrl":
color = "#BBFF00";
break;
case "scriptURL":
color = "#FFBBBB";
break;
case "filterURL":
color = "#BBBBFF";
break;
case "skybox.url":
color = "#BBFFFF";
break;
case "ambientLight.ambientURL":
color = "#FF3300";
}
return color;
}
function brokenURLReport(entityIDs) {
if (entityIDs.length === 0) {
audioFeedback.rejection();
Window.alert("You have nothing selected.");
return;
} else {
var properties;
for (var i = 0; i < entityIDs.length; i++ ){
properties = Entities.getEntityProperties(entityIDs[i]);
if (properties.script.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "script",
url: properties.script,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.serverScripts.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "serverScripts",
url: properties.serverScripts,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Image" && properties.imageURL.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "imageURL",
url: properties.imageURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Material" && properties.materialURL.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "materialURL",
url: properties.materialURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Model" && properties.modelURL.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "modelURL",
url: properties.modelURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (
(properties.type === "Zone" || properties.type === "Model" || properties.type === "ParticleEffect")
&& properties.compoundShapeURL.toLowerCase().startsWith("http")
) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "compoundShapeURL",
url: properties.compoundShapeURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Model" && properties.animation.url.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "animation.url",
url: properties.animation.url,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "ParticleEffect" && properties.textures.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "textures",
url: properties.textures,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "PolyVox" && properties.xTextureURL.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "xTextureURL",
url: properties.xTextureURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "PolyVox" && properties.yTextureURL.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "yTextureURL",
url: properties.yTextureURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "PolyVox" && properties.zTextureURL.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "zTextureURL",
url: properties.zTextureURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Text" && properties.font.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "font",
url: properties.font,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Web" && properties.sourceUrl.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "sourceUrl",
url: properties.sourceUrl,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Web" && properties.scriptURL.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "scriptURL",
url: properties.scriptURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Zone" && properties.filterURL.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "filterURL",
url: properties.filterURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Zone" && properties.skybox.url.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "skybox.url",
url: properties.skybox.url,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
if (properties.type === "Zone" && properties.ambientLight.ambientURL.toLowerCase().startsWith("http")) {
brokenURLReportUrlEntry = {
id: entityIDs[i],
name: properties.name,
type: properties.type,
urlType: "ambientLight.ambientURL",
url: properties.ambientLight.ambientURL,
validity: "NOT_TESTED"
};
brokenURLReportUrlList.push(brokenURLReportUrlEntry);
}
}
if (brokenURLReportUrlList.length === 0) {
audioFeedback.confirmation();
Window.alert("No 'http' URL has been found within the current selection.");
return;
} else {
if (brokenURLReportUrlList.length > MAX_URL_BEFORE_WARNING_FOR_LONG_PROCESS) {
var message = "Number of http URLs found: " + brokenURLReportUrlList.length + "\n The analysis may take time. Do you want to proceed?";
var answer = Window.confirm(message);
if (!answer) {
return;
}
}
if (brokenURLReportOverlayWebWindow !== undefined) {
brokenURLReportOverlayWebWindow.close();
}
brokenURLReportOverlayWebWindow = new OverlayWebWindow({
title: "Broken URL Report",
source: Script.resolvePath("brokenURLReport.html"),
width: 1000,
height: 600
});
brokenURLReportContent = "";
brokenURLReportRequestUrlValidityCheck(brokenURLReportProcessedUrlNo);
}
}
brokenURLReportOverlayWebWindow.webEventReceived.connect(function (message) {
try {
var data = JSON.parse(message);
} catch(e) {
print("brokenURLReport.js: Error parsing JSON");
return;
}
if (data.action === "select") {
selectionManager.setSelections([data.entityID], this);
}
});
}

View file

@ -1969,7 +1969,7 @@ div.entity-list-menu {
}
div.menu-separator{
width: 90%;
width: 100%;
height: 2px;
background-color: #505050;
}
@ -1987,12 +1987,12 @@ button.menu-button {
}
button.menu-button:hover {
background-color: #00B4EF;
background-color: #919191;
border: none;
}
button.menu-button:active {
background-color: #00B4EF;
background-color: #919191;
border: none;
}

View file

@ -7,6 +7,7 @@ setup_hifi_project(Script Qml)
set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/")
setup_memory_debugger()
setup_thread_debugger()
# link in the shared libraries
link_hifi_libraries(shared gl script-engine plugins render-utils ui-plugins input-plugins display-plugins controllers)

View file

@ -4,6 +4,7 @@ set(TARGET_NAME "entities-test")
# This is not a testcase -- just set it up as a regular hifi project
setup_hifi_project(Network Script)
setup_memory_debugger()
setup_thread_debugger()
set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/")
# link in the shared libraries

View file

@ -2,6 +2,7 @@ set(TARGET_NAME gl-test)
# This is not a testcase -- just set it up as a regular hifi project
setup_hifi_project(Quick Gui OpenGL)
setup_memory_debugger()
setup_thread_debugger()
set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/")
link_hifi_libraries(shared gl)
package_libraries_for_deployment()

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