From d0fb09a3bd2a792d8cef2717a4beff8b7808b3c4 Mon Sep 17 00:00:00 2001
From: Simon Walton <simon@highfidelity.io>
Date: Thu, 31 Jan 2019 14:58:58 -0800
Subject: [PATCH 1/3] Assign lowest available suffix when display-names collide

---
 assignment-client/src/avatars/AvatarMixer.cpp | 62 +++++++++++++++----
 assignment-client/src/avatars/AvatarMixer.h   | 20 +++++-
 libraries/avatars/src/AvatarData.cpp          |  9 ++-
 3 files changed, 74 insertions(+), 17 deletions(-)

diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp
index 500772c1b5..f885b8110d 100644
--- a/assignment-client/src/avatars/AvatarMixer.cpp
+++ b/assignment-client/src/avatars/AvatarMixer.cpp
@@ -38,6 +38,19 @@ const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer";
 // FIXME - what we'd actually like to do is send to users at ~50% of their present rate down to 30hz. Assume 90 for now.
 const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45;
 
+const QRegularExpression AvatarMixer::suffixedNamePattern { R"(^\s*(.+)\s*_(\d)+\s*$)" };
+
+// Lexicographic comparison:
+bool AvatarMixer::SessionDisplayName::operator<(const SessionDisplayName& rhs) const {
+    if (_baseName < rhs._baseName) {
+        return true;
+    } else if (rhs._baseName < _baseName) {
+        return false;
+    } else {
+        return _suffix < rhs._suffix;
+    }
+}
+
 AvatarMixer::AvatarMixer(ReceivedMessage& message) :
     ThreadedAssignment(message),
     _slavePool(&_slaveSharedData)
@@ -313,27 +326,40 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
     bool sendIdentity = false;
     if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) {
         AvatarData& avatar = nodeData->getAvatar();
-        const QString& existingBaseDisplayName = nodeData->getBaseDisplayName();
-        if (--_sessionDisplayNames[existingBaseDisplayName].second <= 0) {
-            _sessionDisplayNames.remove(existingBaseDisplayName);
+        const QString& existingBaseDisplayName = nodeData->getAvatar().getSessionDisplayName();
+        if (!existingBaseDisplayName.isEmpty()) {
+            SessionDisplayName existingDisplayName { existingBaseDisplayName };
+
+            auto suffixMatch = suffixedNamePattern.match(existingBaseDisplayName);
+            if (suffixMatch.hasMatch()) {
+                existingDisplayName._baseName = suffixMatch.captured(1);
+                existingDisplayName._suffix = suffixMatch.captured(2).toInt();
+            }
+            _sessionDisplayNames.erase(existingDisplayName);
         }
 
         QString baseName = avatar.getDisplayName().trimmed();
         const QRegularExpression curses { "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?).
         baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk.
-        const QRegularExpression trailingDigits { "\\s*(_\\d+\\s*)?(\\s*\\n[^$]*)?$" }; // trailing whitespace "_123" and any subsequent lines
+        static const QRegularExpression trailingDigits { R"(\s*(_\d+\s*)?(\s*\n[^$]*)?$)" }; // trailing whitespace "_123" and any subsequent lines
         baseName = baseName.remove(trailingDigits);
         if (baseName.isEmpty()) {
             baseName = "anonymous";
         }
 
-        QPair<int, int>& soFar = _sessionDisplayNames[baseName]; // Inserts and answers 0, 0 if not already present, which is what we want.
-        int& highWater = soFar.first;
-        nodeData->setBaseDisplayName(baseName);
-        QString sessionDisplayName = (highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName;
+        SessionDisplayName newDisplayName { baseName };
+        auto nameIter = _sessionDisplayNames.lower_bound(newDisplayName);
+        if (nameIter != _sessionDisplayNames.end() && nameIter->_baseName == baseName) {
+            // Existing instance(s) of name; find first free suffix
+            while (*nameIter == newDisplayName && ++newDisplayName._suffix && ++nameIter != _sessionDisplayNames.end())
+                ;
+        }
+
+        _sessionDisplayNames.insert(newDisplayName);
+        QString sessionDisplayName = (newDisplayName._suffix > 0) ? baseName + "_" + QString::number(newDisplayName._suffix) : baseName;
         avatar.setSessionDisplayName(sessionDisplayName);
-        highWater++;
-        soFar.second++; // refcount
+        nodeData->setBaseDisplayName(baseName);
+
         nodeData->flagIdentityChange();
         nodeData->setAvatarSessionDisplayNameMustChange(false);
         sendIdentity = true;
@@ -410,9 +436,19 @@ void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) {
            QMutexLocker nodeDataLocker(&avatarNode->getLinkedData()->getMutex());
            AvatarMixerClientData* nodeData = dynamic_cast<AvatarMixerClientData*>(avatarNode->getLinkedData());
            const QString& baseDisplayName = nodeData->getBaseDisplayName();
-           // No sense guarding against very rare case of a node with no entry, as this will work without the guard and do one less lookup in the common case.
-           if (--_sessionDisplayNames[baseDisplayName].second <= 0) {
-               _sessionDisplayNames.remove(baseDisplayName);
+           const QString& displayName = nodeData->getAvatar().getSessionDisplayName();
+           SessionDisplayName exitingDisplayName { displayName };
+
+           auto suffixMatch = suffixedNamePattern.match(displayName);
+           if (suffixMatch.hasMatch()) {
+               exitingDisplayName._baseName = suffixMatch.captured(1);
+               exitingDisplayName._suffix = suffixMatch.captured(2).toInt();
+           }
+           auto displayNameIter = _sessionDisplayNames.find(exitingDisplayName);
+           if (displayNameIter == _sessionDisplayNames.end()) {
+               qCDebug(avatars, "Exiting avatar displayname", displayName, "not found");
+           } else {
+               _sessionDisplayNames.erase(displayNameIter);
            }
         }
 
diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h
index 764656a2d5..2992e19b8f 100644
--- a/assignment-client/src/avatars/AvatarMixer.h
+++ b/assignment-client/src/avatars/AvatarMixer.h
@@ -15,6 +15,7 @@
 #ifndef hifi_AvatarMixer_h
 #define hifi_AvatarMixer_h
 
+#include <set>
 #include <shared/RateCounter.h>
 #include <PortableHighResolutionClock.h>
 
@@ -88,7 +89,24 @@ private:
 
     RateCounter<> _broadcastRate;
     p_high_resolution_clock::time_point _lastDebugMessage;
-    QHash<QString, QPair<int, int>> _sessionDisplayNames;
+
+    // Pair of basename + uniquifying integer suffix.
+    struct SessionDisplayName {
+        explicit SessionDisplayName(QString baseName = QString(), int suffix = 0) :
+            _baseName(baseName),
+            _suffix(suffix) { }
+        // Does lexicographic ordering:
+        bool operator<(const SessionDisplayName& rhs) const;
+        bool operator==(const SessionDisplayName& rhs) const {
+            return _baseName == rhs._baseName && _suffix == rhs._suffix;
+        }
+
+        QString _baseName;
+        int _suffix;
+    };
+    static const QRegularExpression suffixedNamePattern;
+
+    std::set<SessionDisplayName> _sessionDisplayNames;
 
     quint64 _displayNameManagementElapsedTime { 0 }; // total time spent in broadcastAvatarData/display name management... since last stats window
     quint64 _ignoreCalculationElapsedTime { 0 };
diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index 4e95774efb..c733cfa291 100755
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -632,9 +632,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
 
     // include jointData if there is room for the most minimal section. i.e. no translations or rotations.
     IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, AvatarDataPacket::minJointDataSize(numJoints)) {
-        // Allow for faux joints + translation bit-vector:
-        const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat)
-            + jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE;
+        // Minimum space required for another rotation joint -
+        // size of joint + following translation bit-vector + translation scale + faux joints:
+        const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize +
+            sizeof(float) + AvatarDataPacket::FAUX_JOINTS_SIZE;
+
         auto startSection = destinationBuffer;
 
         // compute maxTranslationDimension before we send any joint data.
@@ -724,6 +726,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
             const JointData& data = joints[i];
             const JointData& last = lastSentJointData[i];
 
+            // Note minSizeForJoint is conservative since there isn't a following bit-vector + scale.
             if (packetEnd - destinationBuffer >= minSizeForJoint) {
                 if (!data.translationIsDefaultPose) {
                     if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation)

From e4ffc93bc254a4fb52f68ef4770efa730926daee Mon Sep 17 00:00:00 2001
From: Simon Walton <simon@highfidelity.io>
Date: Thu, 31 Jan 2019 15:19:05 -0800
Subject: [PATCH 2/3] Revert a cherry-picked workaround accidently committed

This reverts commit 08b21109c1daac786a67490bfef658a359bf47bb.
Stupid git - I never explicitly added this to the branch!
---
 libraries/avatars/src/AvatarData.cpp | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index c733cfa291..4e95774efb 100755
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -632,11 +632,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
 
     // include jointData if there is room for the most minimal section. i.e. no translations or rotations.
     IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, AvatarDataPacket::minJointDataSize(numJoints)) {
-        // Minimum space required for another rotation joint -
-        // size of joint + following translation bit-vector + translation scale + faux joints:
-        const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize +
-            sizeof(float) + AvatarDataPacket::FAUX_JOINTS_SIZE;
-
+        // Allow for faux joints + translation bit-vector:
+        const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat)
+            + jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE;
         auto startSection = destinationBuffer;
 
         // compute maxTranslationDimension before we send any joint data.
@@ -726,7 +724,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
             const JointData& data = joints[i];
             const JointData& last = lastSentJointData[i];
 
-            // Note minSizeForJoint is conservative since there isn't a following bit-vector + scale.
             if (packetEnd - destinationBuffer >= minSizeForJoint) {
                 if (!data.translationIsDefaultPose) {
                     if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation)

From 322030fccdf481c524c80da328761a350955d6f3 Mon Sep 17 00:00:00 2001
From: Simon Walton <simon@highfidelity.io>
Date: Thu, 31 Jan 2019 16:07:26 -0800
Subject: [PATCH 3/3] Remove unused variable; fix use of qCDebug

---
 assignment-client/src/avatars/AvatarMixer.cpp | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp
index f885b8110d..801f28c6f5 100644
--- a/assignment-client/src/avatars/AvatarMixer.cpp
+++ b/assignment-client/src/avatars/AvatarMixer.cpp
@@ -435,7 +435,6 @@ void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) {
         {  // decrement sessionDisplayNames table and possibly remove
            QMutexLocker nodeDataLocker(&avatarNode->getLinkedData()->getMutex());
            AvatarMixerClientData* nodeData = dynamic_cast<AvatarMixerClientData*>(avatarNode->getLinkedData());
-           const QString& baseDisplayName = nodeData->getBaseDisplayName();
            const QString& displayName = nodeData->getAvatar().getSessionDisplayName();
            SessionDisplayName exitingDisplayName { displayName };
 
@@ -446,7 +445,7 @@ void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) {
            }
            auto displayNameIter = _sessionDisplayNames.find(exitingDisplayName);
            if (displayNameIter == _sessionDisplayNames.end()) {
-               qCDebug(avatars, "Exiting avatar displayname", displayName, "not found");
+               qCDebug(avatars) << "Exiting avatar displayname" << displayName << "not found";
            } else {
                _sessionDisplayNames.erase(displayNameIter);
            }