Merge branch 'master' of https://github.com/highfidelity/hifi into pal-show-lastEditedBy

This commit is contained in:
howard-stearns 2017-01-04 15:48:45 -08:00
commit d78b7d9a28
21 changed files with 454 additions and 91 deletions

View file

@ -50,6 +50,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
auto nodeList = DependencyManager::get<NodeList>(); auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch);
@ -108,6 +109,13 @@ void AvatarMixer::broadcastAvatarData() {
const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES;
const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO;
// only send extra avatar data (avatars out of view, ignored) every Nth AvatarData frame
// Extra avatar data will be sent (AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND/EXTRA_AVATAR_DATA_FRAME_RATIO) times
// per second.
// This value should be a power of two for performance purposes, as the mixer performs a modulo operation every frame
// to determine whether the extra data should be sent.
const int EXTRA_AVATAR_DATA_FRAME_RATIO = 16;
// NOTE: The following code calculates the _performanceThrottlingRatio based on how much the avatar-mixer was // NOTE: The following code calculates the _performanceThrottlingRatio based on how much the avatar-mixer was
// able to sleep. This will eventually be used to ask for an additional avatar-mixer to help out. Currently the value // able to sleep. This will eventually be used to ask for an additional avatar-mixer to help out. Currently the value
// is unused as it is assumed this should not be hit before the avatar-mixer hits the desired bandwidth limit per client. // is unused as it is assumed this should not be hit before the avatar-mixer hits the desired bandwidth limit per client.
@ -205,6 +213,15 @@ void AvatarMixer::broadcastAvatarData() {
// use the data rate specifically for avatar data for FRD adjustment checks // use the data rate specifically for avatar data for FRD adjustment checks
float avatarDataRateLastSecond = nodeData->getOutboundAvatarDataKbps(); float avatarDataRateLastSecond = nodeData->getOutboundAvatarDataKbps();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that are not in the view frustrum
bool getsOutOfView = nodeData->getRequestsDomainListData();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that they've ignored
bool getsIgnoredByMe = getsOutOfView;
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them
bool getsAnyIgnored = getsIgnoredByMe && node->getCanKick();
// Check if it is time to adjust what we send this client based on the observed // Check if it is time to adjust what we send this client based on the observed
// bandwidth to this node. We do this once a second, which is also the window for // bandwidth to this node. We do this once a second, which is also the window for
// the bandwidth reported by node->getOutboundBandwidth(); // the bandwidth reported by node->getOutboundBandwidth();
@ -275,14 +292,14 @@ void AvatarMixer::broadcastAvatarData() {
// or that has ignored the viewing node // or that has ignored the viewing node
if (!otherNode->getLinkedData() if (!otherNode->getLinkedData()
|| otherNode->getUUID() == node->getUUID() || otherNode->getUUID() == node->getUUID()
|| node->isIgnoringNodeWithID(otherNode->getUUID()) || (node->isIgnoringNodeWithID(otherNode->getUUID()) && !getsIgnoredByMe)
|| otherNode->isIgnoringNodeWithID(node->getUUID())) { || (otherNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) {
return false; return false;
} else { } else {
AvatarMixerClientData* otherData = reinterpret_cast<AvatarMixerClientData*>(otherNode->getLinkedData()); AvatarMixerClientData* otherData = reinterpret_cast<AvatarMixerClientData*>(otherNode->getLinkedData());
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData()); AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
// Check to see if the space bubble is enabled // Check to see if the space bubble is enabled
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { if ((node->isIgnoreRadiusEnabled() && !getsIgnoredByMe) || (otherNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
// Define the minimum bubble size // Define the minimum bubble size
static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f); static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
// Define the scale of the box for the current node // Define the scale of the box for the current node
@ -333,7 +350,6 @@ void AvatarMixer::broadcastAvatarData() {
&& (forceSend && (forceSend
|| otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp || otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp
|| distribution(generator) < IDENTITY_SEND_PROBABILITY)) { || distribution(generator) < IDENTITY_SEND_PROBABILITY)) {
sendIdentityPacket(otherNodeData, node); sendIdentityPacket(otherNodeData, node);
} }
@ -349,6 +365,7 @@ void AvatarMixer::broadcastAvatarData() {
maxAvatarDistanceThisFrame = std::max(maxAvatarDistanceThisFrame, distanceToAvatar); maxAvatarDistanceThisFrame = std::max(maxAvatarDistanceThisFrame, distanceToAvatar);
if (distanceToAvatar != 0.0f if (distanceToAvatar != 0.0f
&& !getsOutOfView
&& distribution(generator) > (nodeData->getFullRateDistance() / distanceToAvatar)) { && distribution(generator) > (nodeData->getFullRateDistance() / distanceToAvatar)) {
return; return;
} }
@ -380,15 +397,21 @@ void AvatarMixer::broadcastAvatarData() {
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(), nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNodeData->getLastReceivedSequenceNumber()); otherNodeData->getLastReceivedSequenceNumber());
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
// determine if avatar is in view, to determine how much data to include... // determine if avatar is in view, to determine how much data to include...
glm::vec3 otherNodeBoxScale = (otherNodeData->getPosition() - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f; glm::vec3 otherNodeBoxScale = (otherNodeData->getPosition() - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale); AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
bool isInView = nodeData->otherAvatarInView(otherNodeBox);
// this throttles the extra data to only be sent every Nth message
if (!isInView && getsOutOfView && (lastSeqToReceiver % EXTRA_AVATAR_DATA_FRAME_RATIO > 0)) {
return;
}
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
AvatarData::AvatarDataDetail detail; AvatarData::AvatarDataDetail detail;
if (!nodeData->otherAvatarInView(otherNodeBox)) { if (!isInView && !getsOutOfView) {
detail = AvatarData::MinimumData; detail = AvatarData::MinimumData;
nodeData->incrementAvatarOutOfView(); nodeData->incrementAvatarOutOfView();
} else { } else {
@ -527,6 +550,20 @@ void AvatarMixer::handleViewFrustumPacket(QSharedPointer<ReceivedMessage> messag
} }
} }
void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->getOrCreateLinkedData(senderNode);
if (senderNode->getLinkedData()) {
AvatarMixerClientData* nodeData = dynamic_cast<AvatarMixerClientData*>(senderNode->getLinkedData());
if (nodeData != nullptr) {
bool isRequesting;
message->readPrimitive(&isRequesting);
nodeData->setRequestsDomainListData(isRequesting);
}
}
}
void AvatarMixer::handleAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) { void AvatarMixer::handleAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto nodeList = DependencyManager::get<NodeList>(); auto nodeList = DependencyManager::get<NodeList>();
nodeList->updateNodeWithDataFromPacket(message, senderNode); nodeList->updateNodeWithDataFromPacket(message, senderNode);

View file

@ -42,6 +42,7 @@ private slots:
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message); void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode); void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode); void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void domainSettingsRequestComplete(); void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);

View file

@ -101,6 +101,8 @@ public:
void incrementAvatarOutOfView() { _recentOtherAvatarsOutOfView++; } void incrementAvatarOutOfView() { _recentOtherAvatarsOutOfView++; }
const QString& getBaseDisplayName() { return _baseDisplayName; } const QString& getBaseDisplayName() { return _baseDisplayName; }
void setBaseDisplayName(const QString& baseDisplayName) { _baseDisplayName = baseDisplayName; } void setBaseDisplayName(const QString& baseDisplayName) { _baseDisplayName = baseDisplayName; }
bool getRequestsDomainListData() { return _requestsDomainListData; }
void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; }
private: private:
AvatarSharedPointer _avatar { new AvatarData() }; AvatarSharedPointer _avatar { new AvatarData() };
@ -129,6 +131,7 @@ private:
int _recentOtherAvatarsInView { 0 }; int _recentOtherAvatarsInView { 0 };
int _recentOtherAvatarsOutOfView { 0 }; int _recentOtherAvatarsOutOfView { 0 };
QString _baseDisplayName{}; // The santized key used in determinging unique sessionDisplayName, so that we can remove from dictionary. QString _baseDisplayName{}; // The santized key used in determinging unique sessionDisplayName, so that we can remove from dictionary.
bool _requestsDomainListData { false };
}; };
#endif // hifi_AvatarMixerClientData_h #endif // hifi_AvatarMixerClientData_h

View file

@ -72,6 +72,18 @@ Original.CheckBox {
border.color: hifi.colors.checkboxCheckedBorder border.color: hifi.colors.checkboxCheckedBorder
visible: checked && !pressed || !checked && pressed visible: checked && !pressed || !checked && pressed
} }
Rectangle {
id: disabledOverlay
visible: !enabled
width: boxSize
height: boxSize
radius: boxRadius
border.width: 1
border.color: hifi.colors.baseGrayHighlight
color: hifi.colors.baseGrayHighlight
opacity: 0.5
}
} }
label: Label { label: Label {

View file

@ -35,6 +35,7 @@ Row {
property int usernameTextHeight: 12 property int usernameTextHeight: 12
property real audioLevel: 0.0 property real audioLevel: 0.0
/* User image commented out for now - will probably be re-introduced later.
Column { Column {
id: avatarImage id: avatarImage
// Size // Size
@ -48,10 +49,11 @@ Row {
height: parent.height height: parent.height
} }
} }
*/
Column { Column {
id: textContainer id: textContainer
// Size // Size
width: parent.width - avatarImage.width - parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing width: parent.width - /*avatarImage.width - */parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing
height: contentHeight height: contentHeight
// DisplayName Text // DisplayName Text

View file

@ -40,7 +40,11 @@ Item {
property int myCardHeight: 70 property int myCardHeight: 70
property int rowHeight: 70 property int rowHeight: 70
property int actionButtonWidth: 75 property int actionButtonWidth: 75
property int nameCardWidth: width - actionButtonWidth*(iAmAdmin ? 4 : 2) property int nameCardWidth: width - actionButtonWidth*(iAmAdmin ? 4 : 2) - 4
property var myData: ({displayName: "", userName: "", audioLevel: 0.0}) // valid dummy until set
property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring.
property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities.
property bool iAmAdmin: false
// This contains the current user's NameCard and will contain other information in the future // This contains the current user's NameCard and will contain other information in the future
Rectangle { Rectangle {
@ -85,7 +89,7 @@ Item {
Rectangle { Rectangle {
id: adminTab id: adminTab
// Size // Size
width: actionButtonWidth * 2 - 2 width: actionButtonWidth * 2 + 2
height: 40 height: 40
// Anchors // Anchors
anchors.bottom: myInfo.bottom anchors.bottom: myInfo.bottom
@ -169,7 +173,9 @@ Item {
movable: false movable: false
resizable: false resizable: false
} }
model: userModel model: ListModel {
id: userModel
}
// This Rectangle refers to each Row in the table. // This Rectangle refers to each Row in the table.
rowDelegate: Rectangle { // The only way I know to specify a row height. rowDelegate: Rectangle { // The only way I know to specify a row height.
@ -183,16 +189,17 @@ Item {
// This Item refers to the contents of each Cell // This Item refers to the contents of each Cell
itemDelegate: Item { itemDelegate: Item {
id: itemCell id: itemCell
property bool isCheckBox: typeof(styleData.value) === 'boolean' property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"
property bool isButton: styleData.role === "mute" || styleData.role === "kick"
// This NameCard refers to the cell that contains an avatar's // This NameCard refers to the cell that contains an avatar's
// DisplayName and UserName // DisplayName and UserName
NameCard { NameCard {
id: nameCard id: nameCard
// Properties // Properties
displayName: styleData.value displayName: styleData.value
userName: model.userName userName: model && model.userName
audioLevel: model.audioLevel audioLevel: model && model.audioLevel
visible: !isCheckBox visible: !isCheckBox && !isButton
// Size // Size
width: nameCardWidth width: nameCardWidth
height: parent.height height: parent.height
@ -200,19 +207,69 @@ Item {
anchors.left: parent.left anchors.left: parent.left
} }
// This CheckBox belongs in the columns that contain the action buttons ("Mute", "Ban", etc) // This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now)
// KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox
// will appear in the "hovered" state. Hovering over the checkbox will fix it.
// Clicking on the sides of the sorting header doesn't cause this problem.
// I'm guessing this is a QT bug and not anything I can fix. I spent too long trying to work around it...
// I'm just going to leave the minor visual bug in.
HifiControls.CheckBox { HifiControls.CheckBox {
id: actionCheckBox
visible: isCheckBox visible: isCheckBox
anchors.centerIn: parent anchors.centerIn: parent
checked: model[styleData.role]
// If this is a "Personal Mute" checkbox, disable the checkbox if the "Ignore" checkbox is checked.
enabled: !(styleData.role === "personalMute" && model["ignore"])
boxSize: 24 boxSize: 24
onClicked: { onClicked: {
var newValue = !model[styleData.role] var newValue = !model[styleData.role]
var datum = userData[model.userIndex] userModel.setProperty(model.userIndex, styleData.role, newValue)
datum[styleData.role] = model[styleData.role] = newValue userModelData[model.userIndex][styleData.role] = newValue // Defensive programming
Users[styleData.role](model.sessionId, newValue)
if (styleData.role === "ignore") {
userModel.setProperty(model.userIndex, "personalMute", newValue)
userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming
if (newValue) {
ignored[model.sessionId] = userModelData[model.userIndex]
} else {
delete ignored[model.sessionId]
}
}
// http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript
// I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by
// "checked:" statement above.
checked = Qt.binding(function() { return (model[styleData.role])})
}
}
// This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now)
HifiControls.Button {
id: actionButton
color: 2 // Red
visible: isButton
anchors.centerIn: parent
width: 32
height: 24
onClicked: {
Users[styleData.role](model.sessionId) Users[styleData.role](model.sessionId)
// Just for now, while we cannot undo things: if (styleData.role === "kick") {
userData.splice(model.userIndex, 1) // Just for now, while we cannot undo "Ban":
sortModel() userModel.remove(model.userIndex)
delete userModelData[model.userIndex] // Defensive programming
sortModel()
}
}
// muted/error glyphs
HiFiGlyphs {
text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted
// Size
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]
} }
} }
} }
@ -256,7 +313,6 @@ Item {
onExited: reloadButton.color = (pressed ? hifi.colors.lightGrayText: hifi.colors.darkGray) onExited: reloadButton.color = (pressed ? hifi.colors.lightGrayText: hifi.colors.darkGray)
} }
} }
// Separator between user and admin functions // Separator between user and admin functions
Rectangle { Rectangle {
// Size // Size
@ -309,18 +365,22 @@ Item {
radius: hifi.dimensions.borderRadius radius: hifi.dimensions.borderRadius
} }
Rectangle { Rectangle {
width: Math.min(parent.width * 0.75, 400) width: Math.max(parent.width * 0.75, 400)
height: popupText.contentHeight*2 height: popupText.contentHeight*1.5
anchors.centerIn: parent anchors.centerIn: parent
radius: hifi.dimensions.borderRadius radius: hifi.dimensions.borderRadius
color: "white" color: "white"
FiraSansSemiBold { FiraSansSemiBold {
id: popupText id: popupText
text: "This is temporary text. It will eventually be used to explain what 'Names' means." text: "Bold names in the list are Avatar Display Names.\n" +
"If a Display Name isn't set, a unique Session Display Name is assigned." +
"\n\nAdministrators of this domain can also see the Username or Machine ID associated with each avatar present."
size: hifi.fontSizes.textFieldInput size: hifi.fontSizes.textFieldInput
color: hifi.colors.darkGray color: hifi.colors.darkGray
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: 15
anchors.rightMargin: 15
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
@ -333,11 +393,8 @@ Item {
} }
} }
property var userData: []
property var myData: ({displayName: "", userName: "", audioLevel: 0.0}) // valid dummy until set
property bool iAmAdmin: false
function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml
var i, data = optionalData || userData, length = data.length; var data = optionalData || userModelData, length = data.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
if (data[i].sessionId === sessionId) { if (data[i].sessionId === sessionId) {
return i; return i;
@ -349,11 +406,24 @@ Item {
switch (message.method) { switch (message.method) {
case 'users': case 'users':
var data = message.params; var data = message.params;
var myIndex = findSessionIndex('', data); var index = -1;
iAmAdmin = Users.canKick; index = findSessionIndex('', data);
myData = data[myIndex]; if (index !== -1) {
data.splice(myIndex, 1); iAmAdmin = Users.canKick;
userData = data; myData = data[index];
data.splice(index, 1);
} else {
console.log("This user's data was not found in the user list. PAL will not function properly.");
}
userModelData = data;
for (var ignoredID in ignored) {
index = findSessionIndex(ignoredID);
if (index === -1) { // Add back any missing ignored to the PAL, because they sometimes take a moment to show up.
userModelData.push(ignored[ignoredID]);
} else { // Already appears in PAL; update properties of existing element in model data
userModelData[index] = ignored[ignoredID];
}
}
sortModel(); sortModel();
break; break;
case 'select': case 'select':
@ -378,11 +448,15 @@ Item {
myData.userName = userName; myData.userName = userName;
myCard.userName = userName; // Defensive programming myCard.userName = userName; // Defensive programming
} else { } else {
// Get the index in userModel and userData associated with the passed UUID // Get the index in userModel and userModelData associated with the passed UUID
var userIndex = findSessionIndex(userId); var userIndex = findSessionIndex(userId);
// Set the userName appropriately if (userIndex != -1) {
userModel.get(userIndex).userName = userName; // Set the userName appropriately
userData[userIndex].userName = userName; // Defensive programming userModel.setProperty(userIndex, "userName", userName);
userModelData[userIndex].userName = userName; // Defensive programming
} else {
console.log("updateUsername() called with unknown UUID: ", userId);
}
} }
break; break;
case 'updateAudioLevel': case 'updateAudioLevel':
@ -393,25 +467,28 @@ Item {
myData.audioLevel = audioLevel; myData.audioLevel = audioLevel;
myCard.audioLevel = audioLevel; // Defensive programming myCard.audioLevel = audioLevel; // Defensive programming
} else { } else {
console.log("userid:" + userId);
var userIndex = findSessionIndex(userId); var userIndex = findSessionIndex(userId);
userModel.get(userIndex).audioLevel = audioLevel; if (userIndex != -1) {
userData[userIndex].audioLevel = audioLevel; // Defensive programming userModel.setProperty(userIndex, "audioLevel", audioLevel);
userModelData[userIndex].audioLevel = audioLevel; // Defensive programming
} else {
console.log("updateUsername() called with unknown UUID: ", userId);
}
} }
} }
break; break;
case 'clearIgnored':
ignored = {};
break;
default: default:
console.log('Unrecognized message:', JSON.stringify(message)); console.log('Unrecognized message:', JSON.stringify(message));
} }
} }
ListModel {
id: userModel
}
function sortModel() { function sortModel() {
var sortProperty = table.getColumn(table.sortIndicatorColumn).role; var sortProperty = table.getColumn(table.sortIndicatorColumn).role;
var before = (table.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; var before = (table.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1;
var after = -1 * before; var after = -1 * before;
userData.sort(function (a, b) { userModelData.sort(function (a, b) {
var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase();
switch (true) { switch (true) {
case (aValue < bValue): return before; case (aValue < bValue): return before;
@ -420,9 +497,10 @@ Item {
} }
}); });
table.selection.clear(); table.selection.clear();
userModel.clear(); userModel.clear();
var userIndex = 0; var userIndex = 0;
userData.forEach(function (datum) { userModelData.forEach(function (datum) {
function init(property) { function init(property) {
if (datum[property] === undefined) { if (datum[property] === undefined) {
datum[property] = false; datum[property] = false;
@ -437,7 +515,7 @@ Item {
function noticeSelection() { function noticeSelection() {
var userIds = []; var userIds = [];
table.selection.forEach(function (userIndex) { table.selection.forEach(function (userIndex) {
userIds.push(userData[userIndex].sessionId); userIds.push(userModelData[userIndex].sessionId);
}); });
pal.sendToScript({method: 'selected', params: userIds}); pal.sendToScript({method: 'selected', params: userIds});
} }

View file

@ -44,6 +44,7 @@
#include "Util.h" #include "Util.h"
#include "world.h" #include "world.h"
#include "InterfaceLogging.h" #include "InterfaceLogging.h"
#include "SceneScriptingInterface.h"
#include "SoftAttachmentModel.h" #include "SoftAttachmentModel.h"
#include <Rig.h> #include <Rig.h>
@ -292,7 +293,10 @@ void Avatar::updateAvatarEntities() {
} }
} }
void Avatar::setShouldDie() {
// This will cause the avatar to be shrunk away and removed (the actual Avatar gets removed), but then it comes back.
_owningAvatarMixer.clear();
}
void Avatar::simulate(float deltaTime) { void Avatar::simulate(float deltaTime) {
PerformanceTimer perfTimer("simulate"); PerformanceTimer perfTimer("simulate");
@ -459,6 +463,7 @@ bool Avatar::addToScene(AvatarSharedPointer self, std::shared_ptr<render::Scene>
attachmentModel->addToScene(scene, pendingChanges); attachmentModel->addToScene(scene, pendingChanges);
} }
_inScene = true;
return true; return true;
} }
@ -469,6 +474,7 @@ void Avatar::removeFromScene(AvatarSharedPointer self, std::shared_ptr<render::S
for (auto& attachmentModel : _attachmentModels) { for (auto& attachmentModel : _attachmentModels) {
attachmentModel->removeFromScene(scene, pendingChanges); attachmentModel->removeFromScene(scene, pendingChanges);
} }
_inScene = false;
} }
void Avatar::updateRenderItem(render::PendingChanges& pendingChanges) { void Avatar::updateRenderItem(render::PendingChanges& pendingChanges) {
@ -1328,3 +1334,21 @@ void Avatar::setParentJointIndex(quint16 parentJointIndex) {
} }
} }
} }
void Avatar::addToScene(AvatarSharedPointer myHandle) {
render::ScenePointer scene = qApp->getMain3DScene();
if (scene) {
render::PendingChanges pendingChanges;
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderAvatars() && !DependencyManager::get<NodeList>()->isIgnoringNode(getSessionUUID())) {
addToScene(myHandle, scene, pendingChanges);
}
scene->enqueuePendingChanges(pendingChanges);
} else {
qCWarning(interfaceapp) << "AvatarManager::addAvatar() : Unexpected null scene, possibly during application shutdown";
}
}
void Avatar::ensureInScene(AvatarSharedPointer self) {
if (!_inScene) {
addToScene(self);
}
}

View file

@ -178,6 +178,8 @@ public:
glm::vec3 getUncachedRightPalmPosition() const; glm::vec3 getUncachedRightPalmPosition() const;
glm::quat getUncachedRightPalmRotation() const; glm::quat getUncachedRightPalmRotation() const;
Q_INVOKABLE void setShouldDie();
public slots: public slots:
// FIXME - these should be migrated to use Pose data instead // FIXME - these should be migrated to use Pose data instead
@ -254,6 +256,9 @@ protected:
ThreadSafeValueCache<glm::vec3> _rightPalmPositionCache { glm::vec3() }; ThreadSafeValueCache<glm::vec3> _rightPalmPositionCache { glm::vec3() };
ThreadSafeValueCache<glm::quat> _rightPalmRotationCache { glm::quat() }; ThreadSafeValueCache<glm::quat> _rightPalmRotationCache { glm::quat() };
void addToScene(AvatarSharedPointer self);
void ensureInScene(AvatarSharedPointer self);
private: private:
int _leftPointerGeometryID { 0 }; int _leftPointerGeometryID { 0 };
int _rightPointerGeometryID { 0 }; int _rightPointerGeometryID { 0 };
@ -262,6 +267,7 @@ private:
bool _shouldAnimate { true }; bool _shouldAnimate { true };
bool _shouldSkipRender { false }; bool _shouldSkipRender { false };
bool _isLookAtTarget { false }; bool _isLookAtTarget { false };
bool _inScene { false };
float getBoundingRadius() const; float getBoundingRadius() const;

View file

@ -82,7 +82,11 @@ AvatarManager::AvatarManager(QObject* parent) :
// when we hear that the user has ignored an avatar by session UUID // when we hear that the user has ignored an avatar by session UUID
// immediately remove that avatar instead of waiting for the absence of packets from avatar mixer // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer
connect(nodeList.data(), "ignoredNode", this, "removeAvatar"); connect(nodeList.data(), &NodeList::ignoredNode, this, [=](const QUuid& nodeID, bool enabled) {
if (enabled) {
removeAvatar(nodeID);
}
});
} }
AvatarManager::~AvatarManager() { AvatarManager::~AvatarManager() {
@ -159,6 +163,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
removeAvatar(avatarIterator.key()); removeAvatar(avatarIterator.key());
++avatarIterator; ++avatarIterator;
} else { } else {
avatar->ensureInScene(avatar);
avatar->simulate(deltaTime); avatar->simulate(deltaTime);
++avatarIterator; ++avatarIterator;
@ -219,16 +224,7 @@ AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWe
auto newAvatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer); auto newAvatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer);
auto rawRenderableAvatar = std::static_pointer_cast<Avatar>(newAvatar); auto rawRenderableAvatar = std::static_pointer_cast<Avatar>(newAvatar);
render::ScenePointer scene = qApp->getMain3DScene(); rawRenderableAvatar->addToScene(rawRenderableAvatar);
if (scene) {
render::PendingChanges pendingChanges;
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderAvatars()) {
rawRenderableAvatar->addToScene(rawRenderableAvatar, scene, pendingChanges);
}
scene->enqueuePendingChanges(pendingChanges);
} else {
qCWarning(interfaceapp) << "AvatarManager::addAvatar() : Unexpected null scene, possibly during application shutdown";
}
return newAvatar; return newAvatar;
} }

View file

@ -93,7 +93,6 @@ private:
// virtual overrides // virtual overrides
virtual AvatarSharedPointer newSharedAvatar() override; virtual AvatarSharedPointer newSharedAvatar() override;
virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) override; virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) override;
virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override; virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
QVector<AvatarSharedPointer> _avatarFades; QVector<AvatarSharedPointer> _avatarFades;

View file

@ -112,7 +112,7 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer<ReceivedMessage> mess
// make sure this isn't our own avatar data or for a previously ignored node // make sure this isn't our own avatar data or for a previously ignored node
auto nodeList = DependencyManager::get<NodeList>(); auto nodeList = DependencyManager::get<NodeList>();
if (sessionUUID != _lastOwnerSessionUUID && !nodeList->isIgnoringNode(sessionUUID)) { if (sessionUUID != _lastOwnerSessionUUID && (!nodeList->isIgnoringNode(sessionUUID) || nodeList->getRequestsDomainListData())) {
auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); auto avatar = newOrExistingAvatar(sessionUUID, sendingNode);
// have the matching (or new) avatar parse the data from the packet // have the matching (or new) avatar parse the data from the packet
@ -145,7 +145,7 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
identity.uuid = EMPTY; identity.uuid = EMPTY;
} }
} }
if (!nodeList->isIgnoringNode(identity.uuid)) { if (!nodeList->isIgnoringNode(identity.uuid) || nodeList->getRequestsDomainListData()) {
// mesh URL for a UUID, find avatar in our list // mesh URL for a UUID, find avatar in our list
auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); auto avatar = newOrExistingAvatar(identity.uuid, sendingNode);
avatar->processAvatarIdentity(identity); avatar->processAvatarIdentity(identity);

View file

@ -81,17 +81,24 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) {
_clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile();
} }
void Node::parseIgnoreRequestMessage(QSharedPointer<ReceivedMessage> message) { void Node::parseIgnoreRequestMessage(QSharedPointer<ReceivedMessage> message) {
bool addToIgnore;
message->readPrimitive(&addToIgnore);
while (message->getBytesLeftToRead()) { while (message->getBytesLeftToRead()) {
// parse out the UUID being ignored from the packet // parse out the UUID being ignored from the packet
QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
addIgnoredNode(ignoredUUID); if (addToIgnore) {
addIgnoredNode(ignoredUUID);
} else {
removeIgnoredNode(ignoredUUID);
}
} }
} }
void Node::addIgnoredNode(const QUuid& otherNodeID) { void Node::addIgnoredNode(const QUuid& otherNodeID) {
if (!otherNodeID.isNull() && otherNodeID != _uuid) { if (!otherNodeID.isNull() && otherNodeID != _uuid) {
QReadLocker lock { &_ignoredNodeIDSetLock };
qCDebug(networking) << "Adding" << uuidStringWithoutCurlyBraces(otherNodeID) << "to ignore set for" qCDebug(networking) << "Adding" << uuidStringWithoutCurlyBraces(otherNodeID) << "to ignore set for"
<< uuidStringWithoutCurlyBraces(_uuid); << uuidStringWithoutCurlyBraces(_uuid);
@ -102,6 +109,20 @@ void Node::addIgnoredNode(const QUuid& otherNodeID) {
} }
} }
void Node::removeIgnoredNode(const QUuid& otherNodeID) {
if (!otherNodeID.isNull() && otherNodeID != _uuid) {
// insert/find are read locked concurrently. unsafe_erase is not concurrent, and needs a write lock.
QWriteLocker lock { &_ignoredNodeIDSetLock };
qCDebug(networking) << "Removing" << uuidStringWithoutCurlyBraces(otherNodeID) << "from ignore set for"
<< uuidStringWithoutCurlyBraces(_uuid);
// remove the session UUID from the set of ignored ones for this listening node
_ignoredNodeIDSet.unsafe_erase(otherNodeID);
} else {
qCWarning(networking) << "Node::removeIgnoredNode called with null ID or ID of ignoring node.";
}
}
void Node::parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message) { void Node::parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message) {
bool enabled; bool enabled;
message->readPrimitive(&enabled); message->readPrimitive(&enabled);

View file

@ -21,6 +21,7 @@
#include <QtCore/QSharedPointer> #include <QtCore/QSharedPointer>
#include <QtCore/QUuid> #include <QtCore/QUuid>
#include <QReadLocker>
#include <UUIDHasher.h> #include <UUIDHasher.h>
#include <tbb/concurrent_unordered_set.h> #include <tbb/concurrent_unordered_set.h>
@ -73,7 +74,8 @@ public:
void parseIgnoreRequestMessage(QSharedPointer<ReceivedMessage> message); void parseIgnoreRequestMessage(QSharedPointer<ReceivedMessage> message);
void addIgnoredNode(const QUuid& otherNodeID); void addIgnoredNode(const QUuid& otherNodeID);
bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); } void removeIgnoredNode(const QUuid& otherNodeID);
bool isIgnoringNodeWithID(const QUuid& nodeID) const { QReadLocker lock { &_ignoredNodeIDSetLock }; return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); }
void parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message); void parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message);
friend QDataStream& operator<<(QDataStream& out, const Node& node); friend QDataStream& operator<<(QDataStream& out, const Node& node);
@ -97,6 +99,7 @@ private:
MovingPercentile _clockSkewMovingPercentile; MovingPercentile _clockSkewMovingPercentile;
NodePermissions _permissions; NodePermissions _permissions;
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDSet; tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDSet;
mutable QReadWriteLock _ignoredNodeIDSetLock;
std::atomic_bool _ignoreRadiusEnabled; std::atomic_bool _ignoreRadiusEnabled;
}; };

View file

@ -242,6 +242,10 @@ void NodeList::reset() {
_ignoredSetLock.lockForWrite(); _ignoredSetLock.lockForWrite();
_ignoredNodeIDs.clear(); _ignoredNodeIDs.clear();
_ignoredSetLock.unlock(); _ignoredSetLock.unlock();
// lock and clear our set of personally muted IDs
_personalMutedSetLock.lockForWrite();
_personalMutedNodeIDs.clear();
_personalMutedSetLock.unlock();
// refresh the owner UUID to the NULL UUID // refresh the owner UUID to the NULL UUID
setSessionUUID(QUuid()); setSessionUUID(QUuid());
@ -777,7 +781,7 @@ void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationN
sendPacket(std::move(ignorePacket), *destinationNode); sendPacket(std::move(ignorePacket), *destinationNode);
} }
void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) {
// enumerate the nodes to send a reliable ignore packet to each that can leverage it // enumerate the nodes to send a reliable ignore packet to each that can leverage it
if (!nodeID.isNull() && _sessionUUID != nodeID) { if (!nodeID.isNull() && _sessionUUID != nodeID) {
eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool {
@ -786,25 +790,36 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) {
} else { } else {
return false; return false;
} }
}, [&nodeID, this](const SharedNodePointer& destinationNode) { }, [&nodeID, ignoreEnabled, this](const SharedNodePointer& destinationNode) {
// create a reliable NLPacket with space for the ignore UUID // create a reliable NLPacket with space for the ignore UUID
auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID, true); auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID + sizeof(bool), true);
ignorePacket->writePrimitive(ignoreEnabled);
// write the node ID to the packet // write the node ID to the packet
ignorePacket->write(nodeID.toRfc4122()); ignorePacket->write(nodeID.toRfc4122());
qCDebug(networking) << "Sending packet to ignore node" << uuidStringWithoutCurlyBraces(nodeID); qCDebug(networking) << "Sending packet to" << (destinationNode->getType() == NodeType::AudioMixer ? "AudioMixer" : "AvatarMixer") << "to"
<< (ignoreEnabled ? "ignore" : "unignore") << "node" << uuidStringWithoutCurlyBraces(nodeID);
// send off this ignore packet reliably to the matching node // send off this ignore packet reliably to the matching node
sendPacket(std::move(ignorePacket), *destinationNode); sendPacket(std::move(ignorePacket), *destinationNode);
}); });
QReadLocker setLocker { &_ignoredSetLock }; if (ignoreEnabled) {
QReadLocker ignoredSetLocker{ &_ignoredSetLock }; // read lock for insert
// add this nodeID to our set of ignored IDs QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
_ignoredNodeIDs.insert(nodeID); // add this nodeID to our set of ignored IDs
_ignoredNodeIDs.insert(nodeID);
emit ignoredNode(nodeID); // add this nodeID to our set of personal muted IDs
_personalMutedNodeIDs.insert(nodeID);
emit ignoredNode(nodeID, true);
} else {
QWriteLocker ignoredSetLocker{ &_ignoredSetLock }; // write lock for unsafe_erase
QWriteLocker personalMutedSetLocker{ &_personalMutedSetLock }; // write lock for unsafe_erase
_ignoredNodeIDs.unsafe_erase(nodeID);
_personalMutedNodeIDs.unsafe_erase(nodeID);
emit ignoredNode(nodeID, false);
}
} else { } else {
qWarning() << "NodeList::ignoreNodeBySessionID called with an invalid ID or an ID which matches the current session ID."; qWarning() << "NodeList::ignoreNodeBySessionID called with an invalid ID or an ID which matches the current session ID.";
@ -812,21 +827,94 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) {
} }
bool NodeList::isIgnoringNode(const QUuid& nodeID) const { bool NodeList::isIgnoringNode(const QUuid& nodeID) const {
QReadLocker setLocker { &_ignoredSetLock }; QReadLocker ignoredSetLocker{ &_ignoredSetLock };
return _ignoredNodeIDs.find(nodeID) != _ignoredNodeIDs.cend(); return _ignoredNodeIDs.find(nodeID) != _ignoredNodeIDs.cend();
} }
void NodeList::personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled) {
// cannot personal mute yourself, or nobody
if (!nodeID.isNull() && _sessionUUID != nodeID) {
auto audioMixer = soloNodeOfType(NodeType::AudioMixer);
if (audioMixer) {
if (isIgnoringNode(nodeID)) {
qCDebug(networking) << "You can't personally mute or unmute a node you're already ignoring.";
}
else {
// setup the packet
auto personalMutePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID + sizeof(bool), true);
personalMutePacket->writePrimitive(muteEnabled);
// write the node ID to the packet
personalMutePacket->write(nodeID.toRfc4122());
qCDebug(networking) << "Sending Personal Mute Packet to" << (muteEnabled ? "mute" : "unmute") << "node" << uuidStringWithoutCurlyBraces(nodeID);
sendPacket(std::move(personalMutePacket), *audioMixer);
if (muteEnabled) {
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
// add this nodeID to our set of personal muted IDs
_personalMutedNodeIDs.insert(nodeID);
} else {
QWriteLocker personalMutedSetLocker{ &_personalMutedSetLock }; // write lock for unsafe_erase
_personalMutedNodeIDs.unsafe_erase(nodeID);
}
}
} else {
qWarning() << "Couldn't find audio mixer to send node personal mute request";
}
} else {
qWarning() << "NodeList::personalMuteNodeBySessionID called with an invalid ID or an ID which matches the current session ID.";
}
}
bool NodeList::isPersonalMutingNode(const QUuid& nodeID) const {
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock };
return _personalMutedNodeIDs.find(nodeID) != _personalMutedNodeIDs.cend();
}
void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) {
if (newNode->getType() == NodeType::AudioMixer || newNode->getType() == NodeType::AvatarMixer) { if (newNode->getType() == NodeType::AudioMixer) {
// this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session,
// so send that list along now (assuming it isn't empty) // so send that list along now (assuming it isn't empty)
QReadLocker setLocker { &_ignoredSetLock }; QReadLocker personalMutedSetLocker{ &_personalMutedSetLock };
if (_personalMutedNodeIDs.size() > 0) {
// setup a packet list so we can send the stream of ignore IDs
auto personalMutePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true);
// Force the "enabled" flag in this packet to true
personalMutePacketList->writePrimitive(true);
// enumerate the ignored IDs and write them to the packet list
auto it = _personalMutedNodeIDs.cbegin();
while (it != _personalMutedNodeIDs.end()) {
personalMutePacketList->write(it->toRfc4122());
++it;
}
// send this NLPacketList to the new node
sendPacketList(std::move(personalMutePacketList), *newNode);
}
// also send them the current ignore radius state.
sendIgnoreRadiusStateToNode(newNode);
}
if (newNode->getType() == NodeType::AvatarMixer) {
// this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session,
// so send that list along now (assuming it isn't empty)
QReadLocker ignoredSetLocker{ &_ignoredSetLock };
if (_ignoredNodeIDs.size() > 0) { if (_ignoredNodeIDs.size() > 0) {
// setup a packet list so we can send the stream of ignore IDs // setup a packet list so we can send the stream of ignore IDs
auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true);
// Force the "enabled" flag in this packet to true
ignorePacketList->writePrimitive(true);
// enumerate the ignored IDs and write them to the packet list // enumerate the ignored IDs and write them to the packet list
auto it = _ignoredNodeIDs.cbegin(); auto it = _ignoredNodeIDs.cbegin();
while (it != _ignoredNodeIDs.end()) { while (it != _ignoredNodeIDs.end()) {
@ -930,3 +1018,18 @@ void NodeList::processUsernameFromIDReply(QSharedPointer<ReceivedMessage> messag
emit usernameFromIDReply(nodeUUIDString, username, machineFingerprintString); emit usernameFromIDReply(nodeUUIDString, username, machineFingerprintString);
} }
void NodeList::setRequestsDomainListData(bool isRequesting) {
// Tell the avatar mixer whether I want to receive any additional data to which I might be entitled
if (_requestsDomainListData == isRequesting) {
return;
}
eachMatchingNode([](const SharedNodePointer& node)->bool {
return node->getType() == NodeType::AvatarMixer;
}, [this, isRequesting](const SharedNodePointer& destinationNode) {
auto packet = NLPacket::create(PacketType::RequestsDomainListData, sizeof(bool), true); // reliable
packet->writePrimitive(isRequesting);
sendPacket(std::move(packet), *destinationNode);
});
_requestsDomainListData = isRequesting;
}

View file

@ -76,12 +76,16 @@ public:
void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); } void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); }
void enableIgnoreRadius() { ignoreNodesInRadius(true); } void enableIgnoreRadius() { ignoreNodesInRadius(true); }
void disableIgnoreRadius() { ignoreNodesInRadius(false); } void disableIgnoreRadius() { ignoreNodesInRadius(false); }
void ignoreNodeBySessionID(const QUuid& nodeID); void ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled);
bool isIgnoringNode(const QUuid& nodeID) const; bool isIgnoringNode(const QUuid& nodeID) const;
void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled);
bool isPersonalMutingNode(const QUuid& nodeID) const;
void kickNodeBySessionID(const QUuid& nodeID); void kickNodeBySessionID(const QUuid& nodeID);
void muteNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID);
void requestUsernameFromSessionID(const QUuid& nodeID); void requestUsernameFromSessionID(const QUuid& nodeID);
bool getRequestsDomainListData() { return _requestsDomainListData; }
void setRequestsDomainListData(bool isRequesting);
public slots: public slots:
void reset(); void reset();
@ -109,7 +113,7 @@ public slots:
signals: signals:
void limitOfSilentDomainCheckInsReached(); void limitOfSilentDomainCheckInsReached();
void receivedDomainServerList(); void receivedDomainServerList();
void ignoredNode(const QUuid& nodeID); void ignoredNode(const QUuid& nodeID, bool enabled);
void ignoreRadiusEnabledChanged(bool isIgnored); void ignoreRadiusEnabledChanged(bool isIgnored);
void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint);
@ -153,9 +157,12 @@ private:
HifiSockAddr _assignmentServerSocket; HifiSockAddr _assignmentServerSocket;
bool _isShuttingDown { false }; bool _isShuttingDown { false };
QTimer _keepAlivePingTimer; QTimer _keepAlivePingTimer;
bool _requestsDomainListData;
mutable QReadWriteLock _ignoredSetLock; mutable QReadWriteLock _ignoredSetLock;
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDs; tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDs;
mutable QReadWriteLock _personalMutedSetLock;
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _personalMutedNodeIDs;
void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode);
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true }; Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true };

View file

@ -53,7 +53,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::AvatarData: case PacketType::AvatarData:
case PacketType::BulkAvatarData: case PacketType::BulkAvatarData:
case PacketType::KillAvatar: case PacketType::KillAvatar:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::SessionDisplayName); return static_cast<PacketVersion>(AvatarMixerPacketVersion::Unignore);
case PacketType::ICEServerHeartbeat: case PacketType::ICEServerHeartbeat:
return 18; // ICE Server Heartbeat signing return 18; // ICE Server Heartbeat signing
case PacketType::AssetGetInfo: case PacketType::AssetGetInfo:

View file

@ -104,7 +104,8 @@ public:
UsernameFromIDRequest, UsernameFromIDRequest,
UsernameFromIDReply, UsernameFromIDReply,
ViewFrustum, ViewFrustum,
LAST_PACKET_TYPE = ViewFrustum RequestsDomainListData,
LAST_PACKET_TYPE = RequestsDomainListData
}; };
}; };
@ -207,7 +208,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
SensorToWorldMat, SensorToWorldMat,
HandControllerJoints, HandControllerJoints,
HasKillAvatarReason, HasKillAvatarReason,
SessionDisplayName SessionDisplayName,
Unignore
}; };
enum class DomainConnectRequestVersion : PacketVersion { enum class DomainConnectRequestVersion : PacketVersion {
@ -242,6 +244,7 @@ enum class AudioVersion : PacketVersion {
Exactly10msAudioPackets, Exactly10msAudioPackets,
TerminatingStreamStats, TerminatingStreamStats,
SpaceBubbleChanges, SpaceBubbleChanges,
HasPersonalMute,
}; };
#endif // hifi_PacketHeaders_h #endif // hifi_PacketHeaders_h

View file

@ -21,9 +21,25 @@ UsersScriptingInterface::UsersScriptingInterface() {
connect(nodeList.data(), &NodeList::usernameFromIDReply, this, &UsersScriptingInterface::usernameFromIDReply); connect(nodeList.data(), &NodeList::usernameFromIDReply, this, &UsersScriptingInterface::usernameFromIDReply);
} }
void UsersScriptingInterface::ignore(const QUuid& nodeID) { void UsersScriptingInterface::ignore(const QUuid& nodeID, bool ignoreEnabled) {
// ask the NodeList to ignore this user (based on the session ID of their node) // ask the NodeList to ignore this user (based on the session ID of their node)
DependencyManager::get<NodeList>()->ignoreNodeBySessionID(nodeID); DependencyManager::get<NodeList>()->ignoreNodeBySessionID(nodeID, ignoreEnabled);
}
bool UsersScriptingInterface::getIgnoreStatus(const QUuid& nodeID) {
// ask the NodeList for the Ignore status associated with the given session ID
return DependencyManager::get<NodeList>()->isIgnoringNode(nodeID);
}
void UsersScriptingInterface::personalMute(const QUuid& nodeID, bool muteEnabled) {
// ask the NodeList to mute the user with the given session ID
// "Personal Mute" only applies one way and is not global
DependencyManager::get<NodeList>()->personalMuteNodeBySessionID(nodeID, muteEnabled);
}
bool UsersScriptingInterface::getPersonalMuteStatus(const QUuid& nodeID) {
// ask the NodeList for the Personal Mute status associated with the given session ID
return DependencyManager::get<NodeList>()->isPersonalMutingNode(nodeID);
} }
void UsersScriptingInterface::kick(const QUuid& nodeID) { void UsersScriptingInterface::kick(const QUuid& nodeID) {
@ -61,3 +77,10 @@ void UsersScriptingInterface::disableIgnoreRadius() {
bool UsersScriptingInterface::getIgnoreRadiusEnabled() { bool UsersScriptingInterface::getIgnoreRadiusEnabled() {
return DependencyManager::get<NodeList>()->getIgnoreRadiusEnabled(); return DependencyManager::get<NodeList>()->getIgnoreRadiusEnabled();
} }
bool UsersScriptingInterface::getRequestsDomainListData() {
return DependencyManager::get<NodeList>()->getRequestsDomainListData();
}
void UsersScriptingInterface::setRequestsDomainListData(bool isRequesting) {
DependencyManager::get<NodeList>()->setRequestsDomainListData(isRequesting);
}

View file

@ -24,6 +24,7 @@ class UsersScriptingInterface : public QObject, public Dependency {
SINGLETON_DEPENDENCY SINGLETON_DEPENDENCY
Q_PROPERTY(bool canKick READ getCanKick) Q_PROPERTY(bool canKick READ getCanKick)
Q_PROPERTY(bool requestsDomainListData READ getRequestsDomainListData WRITE setRequestsDomainListData)
public: public:
UsersScriptingInterface(); UsersScriptingInterface();
@ -34,8 +35,31 @@ public slots:
* Ignore another user. * Ignore another user.
* @function Users.ignore * @function Users.ignore
* @param {nodeID} nodeID The node or session ID of the user you want to ignore. * @param {nodeID} nodeID The node or session ID of the user you want to ignore.
* @param {bool} enable True for ignored; false for un-ignored.
*/ */
void ignore(const QUuid& nodeID); void ignore(const QUuid& nodeID, bool ignoreEnabled);
/**jsdoc
* Gets a bool containing whether you have ignored the given Avatar UUID.
* @function Users.getIgnoreStatus
* @param {nodeID} nodeID The node or session ID of the user whose ignore status you want.
*/
bool getIgnoreStatus(const QUuid& nodeID);
/**jsdoc
* Mute another user for you and you only.
* @function Users.personalMute
* @param {nodeID} nodeID The node or session ID of the user you want to mute.
* @param {bool} enable True for enabled; false for disabled.
*/
void personalMute(const QUuid& nodeID, bool muteEnabled);
/**jsdoc
* Requests a bool containing whether you have personally muted the given Avatar UUID.
* @function Users.requestPersonalMuteStatus
* @param {nodeID} nodeID The node or session ID of the user whose personal mute status you want.
*/
bool getPersonalMuteStatus(const QUuid& nodeID);
/**jsdoc /**jsdoc
* Kick another user. * Kick another user.
@ -45,7 +69,7 @@ public slots:
void kick(const QUuid& nodeID); void kick(const QUuid& nodeID);
/**jsdoc /**jsdoc
* Mute another user. * Mute another user for everyone.
* @function Users.mute * @function Users.mute
* @param {nodeID} nodeID The node or session ID of the user you want to mute. * @param {nodeID} nodeID The node or session ID of the user you want to mute.
*/ */
@ -105,6 +129,11 @@ signals:
* @function Users.usernameFromIDReply * @function Users.usernameFromIDReply
*/ */
void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint);
private:
bool getRequestsDomainListData();
void setRequestsDomainListData(bool requests);
bool _requestsDomainListData;
}; };

View file

@ -189,10 +189,14 @@ function populateUserList() {
// Request the username from the given UUID // Request the username from the given UUID
Users.requestUsernameFromID(id); Users.requestUsernameFromID(id);
} }
data.push(avatarPalDatum); // Request personal mute status and ignore status
if (id) { // No overlay for ourself. // from NodeList (as long as we're not requesting it for our own ID)
addAvatarNode(id); if (id) {
avatarPalDatum['personalMute'] = Users.getPersonalMuteStatus(id);
avatarPalDatum['ignore'] = Users.getIgnoreStatus(id);
addAvatarNode(id); // No overlay for ourselves
} }
data.push(avatarPalDatum);
print('PAL data:', JSON.stringify(avatarPalDatum)); print('PAL data:', JSON.stringify(avatarPalDatum));
}); });
pal.sendToQml({method: 'users', params: data}); pal.sendToQml({method: 'users', params: data});
@ -311,9 +315,11 @@ function off() {
} }
triggerMapping.disable(); // It's ok if we disable twice. triggerMapping.disable(); // It's ok if we disable twice.
removeOverlays(); removeOverlays();
Users.requestsDomainListData = false;
} }
function onClicked() { function onClicked() {
if (!pal.visible) { if (!pal.visible) {
Users.requestsDomainListData = true;
populateUserList(); populateUserList();
pal.raise(); pal.raise();
isWired = true; isWired = true;
@ -385,6 +391,14 @@ button.clicked.connect(onClicked);
pal.visibleChanged.connect(onVisibleChanged); pal.visibleChanged.connect(onVisibleChanged);
pal.closed.connect(off); pal.closed.connect(off);
Users.usernameFromIDReply.connect(usernameFromIDReply); Users.usernameFromIDReply.connect(usernameFromIDReply);
function clearIgnoredInQMLAndClosePAL() {
pal.sendToQml({ method: 'clearIgnored' });
if (pal.visible) {
onClicked(); // Close the PAL
}
}
Window.domainChanged.connect(clearIgnoredInQMLAndClosePAL);
Window.domainConnectionRefused.connect(clearIgnoredInQMLAndClosePAL);
// //
// Cleanup. // Cleanup.
@ -395,6 +409,8 @@ Script.scriptEnding.connect(function () {
pal.visibleChanged.disconnect(onVisibleChanged); pal.visibleChanged.disconnect(onVisibleChanged);
pal.closed.disconnect(off); pal.closed.disconnect(off);
Users.usernameFromIDReply.disconnect(usernameFromIDReply); Users.usernameFromIDReply.disconnect(usernameFromIDReply);
Window.domainChanged.disconnect(clearIgnoredInQMLAndClosePAL);
Window.domainConnectionRefused.disconnect(clearIgnoredInQMLAndClosePAL);
off(); off();
}); });