From 85f24f0951c6ef3e34549036b7dd09cc355f7073 Mon Sep 17 00:00:00 2001
From: HifiExperiments <thingsandstuffblog@gmail.com>
Date: Sat, 2 Dec 2023 15:59:52 -0800
Subject: [PATCH 1/2] entity tags

---
 libraries/entities/src/EntityItem.cpp         | 15 ++++
 libraries/entities/src/EntityItem.h           |  5 ++
 .../entities/src/EntityItemProperties.cpp     | 27 +++++++
 libraries/entities/src/EntityItemProperties.h |  4 ++
 .../entities/src/EntityItemPropertiesMacros.h |  7 ++
 libraries/entities/src/EntityPropertyFlags.h  |  1 +
 .../entities/src/EntityScriptingInterface.cpp | 11 +++
 .../entities/src/EntityScriptingInterface.h   | 19 +++++
 libraries/entities/src/EntityTree.cpp         | 36 ++++++++++
 libraries/entities/src/EntityTree.h           |  1 +
 libraries/entities/src/EntityTreeElement.cpp  | 71 +++++++++++++++++++
 libraries/entities/src/EntityTreeElement.h    |  1 +
 libraries/networking/src/udt/PacketHeaders.h  |  1 +
 libraries/octree/src/OctreePacketData.cpp     | 35 +++++++++
 libraries/octree/src/OctreePacketData.h       |  4 ++
 .../script-engine/src/ScriptValueUtils.cpp    | 30 ++++++++
 .../script-engine/src/ScriptValueUtils.h      |  3 +
 17 files changed, 271 insertions(+)

diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp
index b63f0d91ab..ad4345ba9f 100644
--- a/libraries/entities/src/EntityItem.cpp
+++ b/libraries/entities/src/EntityItem.cpp
@@ -104,6 +104,7 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param
     requestedProperties += PROP_IGNORE_PICK_INTERSECTION;
     requestedProperties += PROP_RENDER_WITH_ZONES;
     requestedProperties += PROP_BILLBOARD_MODE;
+    requestedProperties += PROP_TAGS;
     requestedProperties += _grabProperties.getEntityProperties(params);
 
     // Physics
@@ -301,6 +302,7 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet
         APPEND_ENTITY_PROPERTY(PROP_IGNORE_PICK_INTERSECTION, getIgnorePickIntersection());
         APPEND_ENTITY_PROPERTY(PROP_RENDER_WITH_ZONES, getRenderWithZones());
         APPEND_ENTITY_PROPERTY(PROP_BILLBOARD_MODE, (uint32_t)getBillboardMode());
+        APPEND_ENTITY_PROPERTY(PROP_TAGS, getTags());
         withReadLock([&] {
             _grabProperties.appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties,
                 propertyFlags, propertiesDidntFit, propertyCount, appendState);
@@ -874,6 +876,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
     READ_ENTITY_PROPERTY(PROP_IGNORE_PICK_INTERSECTION, bool, setIgnorePickIntersection);
     READ_ENTITY_PROPERTY(PROP_RENDER_WITH_ZONES, QVector<QUuid>, setRenderWithZones);
     READ_ENTITY_PROPERTY(PROP_BILLBOARD_MODE, BillboardMode, setBillboardMode);
+    READ_ENTITY_PROPERTY(PROP_TAGS, QSet<QString>, setTags);
     withWriteLock([&] {
         int bytesFromGrab = _grabProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args,
             propertyFlags, overwriteLocalData,
@@ -1358,6 +1361,7 @@ EntityItemProperties EntityItem::getProperties(const EntityPropertyFlags& desire
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(ignorePickIntersection, getIgnorePickIntersection);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(renderWithZones, getRenderWithZones);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(billboardMode, getBillboardMode);
+    COPY_ENTITY_PROPERTY_TO_PROPERTIES(tags, getTags);
     withReadLock([&] {
         _grabProperties.getProperties(properties);
     });
@@ -1495,6 +1499,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) {
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(ignorePickIntersection, setIgnorePickIntersection);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(renderWithZones, setRenderWithZones);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(billboardMode, setBillboardMode);
+    SET_ENTITY_PROPERTY_FROM_PROPERTIES(tags, setTags);
     withWriteLock([&] {
         bool grabPropertiesChanged = _grabProperties.setProperties(properties);
         somethingChanged |= grabPropertiesChanged;
@@ -3553,3 +3558,13 @@ void EntityItem::setBillboardMode(BillboardMode value) {
         _billboardMode = value;
     });
 }
+
+void EntityItem::setTags(const QSet<QString>& tags) {
+    withWriteLock([&] {
+        _tags = tags;
+    });
+}
+
+QSet<QString> EntityItem::getTags() const {
+    return resultWithReadLock<QSet<QString>>([&] { return _tags; });
+}
\ No newline at end of file
diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h
index ec84b0ccb2..01a2a7c5bf 100644
--- a/libraries/entities/src/EntityItem.h
+++ b/libraries/entities/src/EntityItem.h
@@ -559,6 +559,9 @@ public:
     BillboardMode getBillboardMode() const;
     virtual bool getRotateForPicking() const { return false; }
 
+    void setTags(const QSet<QString>& tags);
+    QSet<QString> getTags() const;
+
 signals:
     void spaceUpdate(std::pair<int32_t, glm::vec4> data);
 
@@ -740,6 +743,8 @@ protected:
 
     bool _cullWithParent { false };
 
+    QSet<QString> _tags;
+
     mutable bool _needsRenderUpdate { false };
 };
 
diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp
index f543169401..205f0ab5f2 100644
--- a/libraries/entities/src/EntityItemProperties.cpp
+++ b/libraries/entities/src/EntityItemProperties.cpp
@@ -424,6 +424,21 @@ void EntityItemProperties::setEntityHostTypeFromString(const QString& entityHost
     }
 }
 
+QVector<QString> EntityItemProperties::getTagsAsVector() const {
+    QVector<QString> tags;
+    for (const QString& tag : _tags) {
+        tags.push_back(tag);
+    }
+    return tags;
+}
+
+void EntityItemProperties::setTagsFromVector(const QVector<QString>& tags) {
+    _tags.clear();
+    for (const QString& tag : tags) {
+        _tags.insert(tag);
+    }
+}
+
 EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
     EntityPropertyFlags changedProperties;
 
@@ -454,6 +469,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
     CHECK_PROPERTY_CHANGE(PROP_IGNORE_PICK_INTERSECTION, ignorePickIntersection);
     CHECK_PROPERTY_CHANGE(PROP_RENDER_WITH_ZONES, renderWithZones);
     CHECK_PROPERTY_CHANGE(PROP_BILLBOARD_MODE, billboardMode);
+    CHECK_PROPERTY_CHANGE(PROP_TAGS, tags);
     changedProperties += _grab.getChangedProperties();
 
     // Physics
@@ -822,6 +838,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
  *     one of the zones in this list.
  * @property {BillboardMode} billboardMode="none" - Whether the entity is billboarded to face the camera.  Use the rotation
  *     property to control which axis is facing you.
+ * @property {string[]} tags=[] - A set of tags describing this entity.
  *
  * @property {Entities.Grab} grab - The entity's grab-related properties.
  *
@@ -1615,6 +1632,7 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IGNORE_PICK_INTERSECTION, ignorePickIntersection);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RENDER_WITH_ZONES, renderWithZones);
     COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BILLBOARD_MODE, billboardMode, getBillboardModeAsString());
+    COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_TAGS, tags, getTagsAsVector());
     _grab.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
 
     // Physics
@@ -2031,6 +2049,7 @@ void EntityItemProperties::copyFromScriptValue(const ScriptValue& object, bool h
     COPY_PROPERTY_FROM_QSCRIPTVALUE(ignorePickIntersection, bool, setIgnorePickIntersection);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(renderWithZones, qVectorQUuid, setRenderWithZones);
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(billboardMode, BillboardMode);
+    COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(tags, qVectorQString, setTagsFromVector, getTagsAsVector);
     _grab.copyFromScriptValue(object, namesSet, _defaultSettings);
 
     // Physics
@@ -2317,6 +2336,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
     COPY_PROPERTY_IF_CHANGED(ignorePickIntersection);
     COPY_PROPERTY_IF_CHANGED(renderWithZones);
     COPY_PROPERTY_IF_CHANGED(billboardMode);
+    COPY_PROPERTY_IF_CHANGED(tags);
     _grab.merge(other._grab);
 
     // Physics
@@ -2605,6 +2625,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
         ADD_PROPERTY_TO_MAP(PROP_IGNORE_PICK_INTERSECTION, IgnorePickIntersection, ignorePickIntersection, bool);
         ADD_PROPERTY_TO_MAP(PROP_RENDER_WITH_ZONES, RenderWithZones, renderWithZones, QVector<QUuid>);
         ADD_PROPERTY_TO_MAP(PROP_BILLBOARD_MODE, BillboardMode, billboardMode, BillboardMode);
+        ADD_PROPERTY_TO_MAP(PROP_TAGS, Tags, tags, QSet<QString>);
         { // Grab
             ADD_GROUP_PROPERTY_TO_MAP(PROP_GRAB_GRABBABLE, Grab, grab, Grabbable, grabbable);
             ADD_GROUP_PROPERTY_TO_MAP(PROP_GRAB_KINEMATIC, Grab, grab, GrabKinematic, grabKinematic);
@@ -3087,6 +3108,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
             APPEND_ENTITY_PROPERTY(PROP_IGNORE_PICK_INTERSECTION, properties.getIgnorePickIntersection());
             APPEND_ENTITY_PROPERTY(PROP_RENDER_WITH_ZONES, properties.getRenderWithZones());
             APPEND_ENTITY_PROPERTY(PROP_BILLBOARD_MODE, (uint32_t)properties.getBillboardMode());
+            APPEND_ENTITY_PROPERTY(PROP_TAGS, properties.getTags());
             _staticGrab.setProperties(properties);
             _staticGrab.appendToEditPacket(packetData, requestedProperties, propertyFlags,
                                            propertiesDidntFit, propertyCount, appendState);
@@ -3567,6 +3589,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_IGNORE_PICK_INTERSECTION, bool, setIgnorePickIntersection);
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_RENDER_WITH_ZONES, QVector<QUuid>, setRenderWithZones);
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_BILLBOARD_MODE, BillboardMode, setBillboardMode);
+    READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_TAGS, QSet<QString>, setTags);
     properties.getGrab().decodeFromEditPacket(propertyFlags, dataAt, processedBytes);
 
     // Physics
@@ -3959,6 +3982,7 @@ void EntityItemProperties::markAllChanged() {
     _ignorePickIntersectionChanged = true;
     _renderWithZonesChanged = true;
     _billboardModeChanged = true;
+    _tagsChanged = true;
     _grab.markAllChanged();
 
     // Physics
@@ -4358,6 +4382,9 @@ QList<QString> EntityItemProperties::listChangedProperties() {
     if (billboardModeChanged()) {
         out += "billboardMode";
     }
+    if (tagsChanged()) {
+        out += "tags";
+    }
     getGrab().listChangedProperties(out);
 
     // Physics
diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h
index 283d14c4cc..74cd9c4f15 100644
--- a/libraries/entities/src/EntityItemProperties.h
+++ b/libraries/entities/src/EntityItemProperties.h
@@ -203,6 +203,7 @@ public:
     DEFINE_PROPERTY(PROP_IGNORE_PICK_INTERSECTION, IgnorePickIntersection, ignorePickIntersection, bool, false);
     DEFINE_PROPERTY_REF(PROP_RENDER_WITH_ZONES, RenderWithZones, renderWithZones, QVector<QUuid>, QVector<QUuid>());
     DEFINE_PROPERTY_REF_ENUM(PROP_BILLBOARD_MODE, BillboardMode, billboardMode, BillboardMode, BillboardMode::NONE);
+    DEFINE_PROPERTY_REF(PROP_TAGS, Tags, tags, QSet<QString>, QSet<QString>());
     DEFINE_PROPERTY_GROUP(Grab, grab, GrabPropertyGroup);
 
     // Physics
@@ -498,6 +499,9 @@ protected:
     QString getCollisionMaskAsString() const;
     void setCollisionMaskFromString(const QString& maskString);
 
+    QVector<QString> getTagsAsVector() const;
+    void setTagsFromVector(const QVector<QString>& tags);
+
 private:
     QUuid _id;
     bool _idSet;
diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h
index d6ee64ba9a..3b6e424663 100644
--- a/libraries/entities/src/EntityItemPropertiesMacros.h
+++ b/libraries/entities/src/EntityItemPropertiesMacros.h
@@ -125,6 +125,7 @@ inline ScriptValue convertScriptValue(ScriptEngine* e, const QVector<glm::quat>&
 inline ScriptValue convertScriptValue(ScriptEngine* e, const QVector<bool>& v) {return qVectorBoolToScriptValue(e, v); }
 inline ScriptValue convertScriptValue(ScriptEngine* e, const QVector<float>& v) { return qVectorFloatToScriptValue(e, v); }
 inline ScriptValue convertScriptValue(ScriptEngine* e, const QVector<QUuid>& v) { return qVectorQUuidToScriptValue(e, v); }
+inline ScriptValue convertScriptValue(ScriptEngine* e, const QVector<QString>& v) { return qVectorQStringToScriptValue(e, v); }
 
 inline ScriptValue convertScriptValue(ScriptEngine* e, const QRect& v) { return qRectToScriptValue(e, v); }
 
@@ -223,6 +224,7 @@ typedef QVector<glm::quat> qVectorQuat;
 typedef QVector<bool> qVectorBool;
 typedef QVector<float> qVectorFloat;
 typedef QVector<QUuid> qVectorQUuid;
+typedef QVector<QString> qVectorQString;
 inline float float_convertFromScriptValue(const ScriptValue& v, bool& isValid) { return v.toVariant().toFloat(&isValid); }
 inline quint64 quint64_convertFromScriptValue(const ScriptValue& v, bool& isValid) { return v.toVariant().toULongLong(&isValid); }
 inline quint32 quint32_convertFromScriptValue(const ScriptValue& v, bool& isValid) {
@@ -305,6 +307,11 @@ inline qVectorQUuid qVectorQUuid_convertFromScriptValue(const ScriptValue& v, bo
     return qVectorQUuidFromScriptValue(v);
 }
 
+inline qVectorQString qVectorQString_convertFromScriptValue(const ScriptValue& v, bool& isValid) {
+    isValid = true;
+    return qVectorQStringFromScriptValue(v);
+}
+
 inline glm::quat quat_convertFromScriptValue(const ScriptValue& v, bool& isValid) {
     isValid = false; /// assume it can't be converted
     ScriptValue x = v.property("x");
diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h
index e0b5a04094..555ac0a2de 100644
--- a/libraries/entities/src/EntityPropertyFlags.h
+++ b/libraries/entities/src/EntityPropertyFlags.h
@@ -46,6 +46,7 @@ enum EntityPropertyList {
     PROP_IGNORE_PICK_INTERSECTION,
     PROP_RENDER_WITH_ZONES,
     PROP_BILLBOARD_MODE,
+    PROP_TAGS,
     // Grab
     PROP_GRAB_GRABBABLE,
     PROP_GRAB_KINEMATIC,
diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp
index 2c36b7ddbc..4acf9b8d1b 100644
--- a/libraries/entities/src/EntityScriptingInterface.cpp
+++ b/libraries/entities/src/EntityScriptingInterface.cpp
@@ -1457,6 +1457,17 @@ QVector<QUuid> EntityScriptingInterface::findEntitiesByName(const QString entity
     return result;
 }
 
+QVector<QUuid> EntityScriptingInterface::findEntitiesByTags(const QVector<QString> entityTags, const glm::vec3& center, float radius, bool caseSensitiveSearch) const {
+    QVector<QUuid> result;
+    if (_entityTree) {
+        _entityTree->withReadLock([&] {
+            unsigned int searchFilter = PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES) | PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES);
+            _entityTree->evalEntitiesInSphereWithTags(center, radius, entityTags, caseSensitiveSearch, PickFilter(searchFilter), result);
+        });
+    }
+    return result;
+}
+
 RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersection(const PickRay& ray, bool precisionPicking,
         const ScriptValue& entityIdsToInclude, const ScriptValue& entityIdsToDiscard, bool visibleOnly, bool collidableOnly) const {
     PROFILE_RANGE(script_entities, __FUNCTION__);
diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h
index 198cac005c..c677bdf0a1 100644
--- a/libraries/entities/src/EntityScriptingInterface.h
+++ b/libraries/entities/src/EntityScriptingInterface.h
@@ -789,6 +789,25 @@ public slots:
     Q_INVOKABLE QVector<QUuid> findEntitiesByName(const QString entityName, const glm::vec3& center, float radius,
         bool caseSensitiveSearch = false) const;
 
+    /*@jsdoc
+     * Finds all domain and avatar entities with particular tags that intersect a sphere.
+     * <p><strong>Note:</strong> Server entity scripts only find entities that have a server entity script
+     * running in them or a parent entity. You can apply a dummy script to entities that you want found in a search.</p>
+     * @function Entities.findEntitiesByTags
+     * @param {string[]} entityTags - The tags of the entity to search for.
+     * @param {Vec3} center - The point about which to search.
+     * @param {number} radius - The radius within which to search.
+     * @param {boolean} [caseSensitive=false] - <code>true</code> if the search is case-sensitive, <code>false</code> if it is
+     *     case-insensitive.
+     * @returns {Uuid[]} An array of entity IDs that have the specified name and intersect the search sphere. The array is
+     *     empty if no entities could be found.
+     * @example <caption>Report the number of entities with the tag, "Light-Target".</caption>
+     * var entityIDs = Entities.findEntitiesByTags(["Light-Target"], MyAvatar.position, 10, false);
+     * print("Number of entities with the tag Light-Target: " + entityIDs.length);
+     */
+    Q_INVOKABLE QVector<QUuid> findEntitiesByTags(const QVector<QString> entityTags, const glm::vec3& center, float radius,
+        bool caseSensitiveSearch = false) const;
+
     /*@jsdoc
      * Finds the first avatar or domain entity intersected by a {@link PickRay}. <code>Light</code> and <code>Zone</code> 
      * entities are not intersected unless they've been configured as pickable using 
diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp
index fb7fbe6499..5f88d7191c 100644
--- a/libraries/entities/src/EntityTree.cpp
+++ b/libraries/entities/src/EntityTree.cpp
@@ -1110,6 +1110,42 @@ void EntityTree::evalEntitiesInSphereWithName(const glm::vec3& center, float rad
     foundEntities.swap(args.entities);
 }
 
+class FindEntitiesInSphereWithTagsArgs {
+public:
+    // Inputs
+    glm::vec3 position;
+    float targetRadius;
+    QVector<QString> tags;
+    bool caseSensitive;
+    PickFilter searchFilter;
+
+    // Outputs
+    QVector<QUuid> entities;
+};
+
+bool evalInSphereWithTagsOperation(const OctreeElementPointer& element, void* extraData) {
+    FindEntitiesInSphereWithTagsArgs* args = static_cast<FindEntitiesInSphereWithTagsArgs*>(extraData);
+    glm::vec3 penetration;
+    bool sphereIntersection = element->getAACube().findSpherePenetration(args->position, args->targetRadius, penetration);
+
+    // If this element contains the point, then search it...
+    if (sphereIntersection) {
+        EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
+        entityTreeElement->evalEntitiesInSphereWithTags(args->position, args->targetRadius, args->tags, args->caseSensitive, args->searchFilter, args->entities);
+        return true; // keep searching in case children have closer entities
+    }
+
+    // if this element doesn't contain the point, then none of it's children can contain the point, so stop searching
+    return false;
+}
+
+// NOTE: assumes caller has handled locking
+void EntityTree::evalEntitiesInSphereWithTags(const glm::vec3& center, float radius, const QVector<QString>& tags, bool caseSensitive, PickFilter searchFilter, QVector<QUuid>& foundEntities) {
+    FindEntitiesInSphereWithTagsArgs args = { center, radius, tags, caseSensitive, searchFilter, QVector<QUuid>() };
+    recurseTreeWithOperation(evalInSphereWithTagsOperation, &args);
+    foundEntities.swap(args.entities);
+}
+
 class FindEntitiesInCubeArgs {
 public:
     // Inputs
diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h
index d5c3a74eb7..1161bec6e9 100644
--- a/libraries/entities/src/EntityTree.h
+++ b/libraries/entities/src/EntityTree.h
@@ -138,6 +138,7 @@ public:
     void evalEntitiesInSphere(const glm::vec3& center, float radius, PickFilter searchFilter, QVector<QUuid>& foundEntities);
     void evalEntitiesInSphereWithType(const glm::vec3& center, float radius, EntityTypes::EntityType type, PickFilter searchFilter, QVector<QUuid>& foundEntities);
     void evalEntitiesInSphereWithName(const glm::vec3& center, float radius, const QString& name, bool caseSensitive, PickFilter searchFilter, QVector<QUuid>& foundEntities);
+    void evalEntitiesInSphereWithTags(const glm::vec3& center, float radius, const QVector<QString>& tags, bool caseSensitive, PickFilter searchFilter, QVector<QUuid>& foundEntities);
     void evalEntitiesInCube(const AACube& cube, PickFilter searchFilter, QVector<QUuid>& foundEntities);
     void evalEntitiesInBox(const AABox& box, PickFilter searchFilter, QVector<QUuid>& foundEntities);
     void evalEntitiesInFrustum(const ViewFrustum& frustum, PickFilter searchFilter, QVector<QUuid>& foundEntities);
diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp
index f330058a78..030c3082c2 100644
--- a/libraries/entities/src/EntityTreeElement.cpp
+++ b/libraries/entities/src/EntityTreeElement.cpp
@@ -602,6 +602,77 @@ void EntityTreeElement::evalEntitiesInSphereWithName(const glm::vec3& position,
     });
 }
 
+void EntityTreeElement::evalEntitiesInSphereWithTags(const glm::vec3& position, float radius, const QVector<QString>& tags, bool caseSensitive, PickFilter searchFilter, QVector<QUuid>& foundEntities) const {
+    forEachEntity([&](EntityItemPointer entity) {
+        if (!checkFilterSettings(entity, searchFilter)) {
+            return;
+        }
+
+        QSet<QString> entityTags = entity->getTags();
+        for (const QString& tag : tags) {
+            if (caseSensitive && !entityTags.contains(tag)) {
+                return;
+            } else {
+                const QString lowerTag = tag.toLower();
+                bool found = false;
+                for (const QString& entityTag : entityTags) {
+                    if (lowerTag == entityTag.toLower()) {
+                        found = true;
+                    }
+                }
+                if (!found) {
+                    return;
+                }
+            }
+        }
+
+        bool success;
+        AABox entityBox = entity->getAABox(success);
+
+        // if the sphere doesn't intersect with our world frame AABox, we don't need to consider the more complex case
+        glm::vec3 penetration;
+        if (success && entityBox.findSpherePenetration(position, radius, penetration)) {
+
+            glm::vec3 dimensions = entity->getScaledDimensions();
+
+            // FIXME - consider allowing the entity to determine penetration so that
+            //         entities could presumably do actual hull testing if they wanted to
+            // FIXME - handle entity->getShapeType() == SHAPE_TYPE_SPHERE case better in particular
+            //         can we handle the ellipsoid case better? We only currently handle perfect spheres
+            //         with centered registration points
+            if (entity->getShapeType() == SHAPE_TYPE_SPHERE && (dimensions.x == dimensions.y && dimensions.y == dimensions.z)) {
+
+                // NOTE: entity->getRadius() doesn't return the true radius, it returns the radius of the
+                //       maximum bounding sphere, which is actually larger than our actual radius
+                float entityTrueRadius = dimensions.x / 2.0f;
+                bool success;
+                glm::vec3 center = entity->getCenterPosition(success);
+
+                if (success && findSphereSpherePenetration(position, radius, center, entityTrueRadius, penetration)) {
+                    foundEntities.push_back(entity->getID());
+                }
+            } else {
+                // determine the worldToEntityMatrix that doesn't include scale because
+                // we're going to use the registration aware aa box in the entity frame
+                glm::mat4 translation = glm::translate(entity->getWorldPosition());
+                glm::mat4 rotation = glm::mat4_cast(entity->getWorldOrientation());
+                glm::mat4 entityToWorldMatrix = translation * rotation;
+                glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix);
+
+                glm::vec3 registrationPoint = entity->getRegistrationPoint();
+                glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot();
+
+                AABox entityFrameBox(corner, dimensions);
+
+                glm::vec3 entityFrameSearchPosition = glm::vec3(worldToEntityMatrix * glm::vec4(position, 1.0f));
+                if (entityFrameBox.findSpherePenetration(entityFrameSearchPosition, radius, penetration)) {
+                    foundEntities.push_back(entity->getID());
+                }
+            }
+        }
+    });
+}
+
 void EntityTreeElement::evalEntitiesInCube(const AACube& cube, PickFilter searchFilter, QVector<QUuid>& foundEntities) const {
     forEachEntity([&](EntityItemPointer entity) {
         if (!checkFilterSettings(entity, searchFilter)) {
diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h
index dab56132c9..951481fcb9 100644
--- a/libraries/entities/src/EntityTreeElement.h
+++ b/libraries/entities/src/EntityTreeElement.h
@@ -177,6 +177,7 @@ public:
     void evalEntitiesInSphere(const glm::vec3& position, float radius, PickFilter searchFilter, QVector<QUuid>& foundEntities) const;
     void evalEntitiesInSphereWithType(const glm::vec3& position, float radius, EntityTypes::EntityType type, PickFilter searchFilter, QVector<QUuid>& foundEntities) const;
     void evalEntitiesInSphereWithName(const glm::vec3& position, float radius, const QString& name, bool caseSensitive, PickFilter searchFilter, QVector<QUuid>& foundEntities) const;
+    void evalEntitiesInSphereWithTags(const glm::vec3& position, float radius, const QVector<QString>& tags, bool caseSensitive, PickFilter searchFilter, QVector<QUuid>& foundEntities) const;
     void evalEntitiesInCube(const AACube& cube, PickFilter searchFilter, QVector<QUuid>& foundEntities) const;
     void evalEntitiesInBox(const AABox& box, PickFilter searchFilter, QVector<QUuid>& foundEntities) const;
     void evalEntitiesInFrustum(const ViewFrustum& frustum, PickFilter searchFilter, QVector<QUuid>& foundEntities) const;
diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h
index 3badfdf418..55e183d00b 100644
--- a/libraries/networking/src/udt/PacketHeaders.h
+++ b/libraries/networking/src/udt/PacketHeaders.h
@@ -290,6 +290,7 @@ enum class EntityVersion : PacketVersion {
     UserAgent,
     AllBillboardMode,
     TextAlignment,
+    EntityTags,
 
     // Add new versions above here
     NUM_PACKET_TYPE,
diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp
index 3966fcf86f..c13d58226b 100644
--- a/libraries/octree/src/OctreePacketData.cpp
+++ b/libraries/octree/src/OctreePacketData.cpp
@@ -496,6 +496,24 @@ bool OctreePacketData::appendValue(const QVector<QUuid>& value) {
     return success;
 }
 
+bool OctreePacketData::appendValue(const QSet<QString>& value) {
+    QVector<QString> valueVector;
+    for (const QString& valueString : value) {
+        valueVector.push_back(valueString);
+    }
+
+    uint16_t qVecSize = value.size();
+    bool success = appendValue(qVecSize);
+    if (success) {
+        success = append((const unsigned char*)valueVector.constData(), qVecSize * sizeof(QString));
+        if (success) {
+            _bytesOfValues += qVecSize * sizeof(QString);
+            _totalBytesOfValues += qVecSize * sizeof(QString);
+        }
+    }
+    return success;
+}
+
 bool OctreePacketData::appendValue(const glm::quat& value) {
     const size_t VALUES_PER_QUAT = 4;
     const size_t PACKED_QUAT_SIZE = sizeof(uint16_t) * VALUES_PER_QUAT;
@@ -802,6 +820,23 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto
     return sizeof(uint16_t) + length * sizeof(QUuid);
 }
 
+int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QSet<QString>& result) {
+    QVector<QString> resultVector;
+
+    uint16_t length;
+    memcpy(&length, dataBytes, sizeof(uint16_t));
+    dataBytes += sizeof(length);
+    resultVector.resize(length);
+    memcpy(resultVector.data(), dataBytes, length * sizeof(QString));
+
+    result.clear();
+    for (const QString& resultString : resultVector) {
+        result.insert(resultString);
+    }
+
+    return sizeof(uint16_t) + length * sizeof(QString);
+}
+
 int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QByteArray& result) {
     uint16_t length;
     memcpy(&length, dataBytes, sizeof(length));
diff --git a/libraries/octree/src/OctreePacketData.h b/libraries/octree/src/OctreePacketData.h
index 583f090942..745551eb22 100644
--- a/libraries/octree/src/OctreePacketData.h
+++ b/libraries/octree/src/OctreePacketData.h
@@ -190,6 +190,9 @@ public:
     /// appends a QVector of QUuids to the end of the stream, may fail if new data stream is too long to fit in packet
     bool appendValue(const QVector<QUuid>& value);
 
+    /// appends a QSet of QStrings to the end of the stream, may fail if new data stream is too long to fit in packet
+    bool appendValue(const QSet<QString>& value);
+
     /// appends a packed quat to the end of the stream, may fail if new data stream is too long to fit in packet
     bool appendValue(const glm::quat& value);
 
@@ -290,6 +293,7 @@ public:
     static int unpackDataFromBytes(const unsigned char* dataBytes, QVector<float>& result);
     static int unpackDataFromBytes(const unsigned char* dataBytes, QVector<bool>& result);
     static int unpackDataFromBytes(const unsigned char* dataBytes, QVector<QUuid>& result);
+    static int unpackDataFromBytes(const unsigned char* dataBytes, QSet<QString>& result);
     static int unpackDataFromBytes(const unsigned char* dataBytes, QByteArray& result);
     static int unpackDataFromBytes(const unsigned char* dataBytes, AACube& result);
     static int unpackDataFromBytes(const unsigned char* dataBytes, QRect& result);
diff --git a/libraries/script-engine/src/ScriptValueUtils.cpp b/libraries/script-engine/src/ScriptValueUtils.cpp
index 5bada98f15..f5808080ba 100644
--- a/libraries/script-engine/src/ScriptValueUtils.cpp
+++ b/libraries/script-engine/src/ScriptValueUtils.cpp
@@ -862,6 +862,14 @@ bool quuidFromScriptValue(const ScriptValue& object, QUuid& uuid) {
     return true;
 }
 
+ScriptValue qStringToScriptValue(ScriptEngine* engine, const QString& string) {
+    if (string.isNull()) {
+        return engine->nullValue();
+    }
+    ScriptValue obj(engine->newValue(string));
+    return obj;
+}
+
 /*@jsdoc
  * A 2D size value.
  * @typedef {object} Size
@@ -1029,3 +1037,25 @@ QVector<EntityItemID> qVectorEntityItemIDFromScriptValue(const ScriptValue& arra
     }
     return newVector;
 }
+
+ScriptValue qVectorQStringToScriptValue(ScriptEngine* engine, const QVector<QString>& vector) {
+    ScriptValue array = engine->newArray();
+    for (int i = 0; i < vector.size(); i++) {
+        array.setProperty(i, qStringToScriptValue(engine, vector.at(i)));
+    }
+    return array;
+}
+
+QVector<QString> qVectorQStringFromScriptValue(const ScriptValue& array) {
+    if (!array.isArray()) {
+        return QVector<QString>();
+    }
+    QVector<QString> newVector;
+    int length = array.property("length").toInteger();
+    newVector.reserve(length);
+    for (int i = 0; i < length; i++) {
+        QString string = array.property(i).toString();
+        newVector << string;
+    }
+    return newVector;
+}
\ No newline at end of file
diff --git a/libraries/script-engine/src/ScriptValueUtils.h b/libraries/script-engine/src/ScriptValueUtils.h
index 45fee61cc4..0a206d1aa5 100644
--- a/libraries/script-engine/src/ScriptValueUtils.h
+++ b/libraries/script-engine/src/ScriptValueUtils.h
@@ -224,6 +224,9 @@ ScriptValue qVectorQUuidToScriptValue(ScriptEngine* engine, const QVector<QUuid>
 bool qVectorQUuidFromScriptValue(const ScriptValue& array, QVector<QUuid>& vector);
 QVector<QUuid> qVectorQUuidFromScriptValue(const ScriptValue& array);
 
+ScriptValue qVectorQStringToScriptValue(ScriptEngine* engine, const QVector<QString>& vector);
+QVector<QString> qVectorQStringFromScriptValue(const ScriptValue& array);
+
 class AACube;
 ScriptValue aaCubeToScriptValue(ScriptEngine* engine, const AACube& aaCube);
 bool aaCubeFromScriptValue(const ScriptValue& object, AACube& aaCube);

From f99a2fa4056ee58a840dc4cfbe18473b8b79b962 Mon Sep 17 00:00:00 2001
From: HifiExperiments <thingsandstuffblog@gmail.com>
Date: Sat, 30 Dec 2023 13:41:56 -0800
Subject: [PATCH 2/2] break when tag found

---
 libraries/entities/src/EntityTreeElement.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp
index 030c3082c2..32c791da33 100644
--- a/libraries/entities/src/EntityTreeElement.cpp
+++ b/libraries/entities/src/EntityTreeElement.cpp
@@ -618,6 +618,7 @@ void EntityTreeElement::evalEntitiesInSphereWithTags(const glm::vec3& position,
                 for (const QString& entityTag : entityTags) {
                     if (lowerTag == entityTag.toLower()) {
                         found = true;
+                        break;
                     }
                 }
                 if (!found) {