From 9315c546d3ac9cfb611e56d12918290649fbb56c Mon Sep 17 00:00:00 2001
From: Simon Walton <simon@highfidelity.io>
Date: Mon, 3 Dec 2018 16:26:47 -0800
Subject: [PATCH 1/4] Initial version - move updateJoints() to derived class

---
 .../src/avatars-renderer/Avatar.cpp           | 28 ++++++++++++++++++
 .../src/avatars-renderer/Avatar.h             |  3 ++
 libraries/avatars/src/AvatarData.cpp          | 29 ++-----------------
 3 files changed, 33 insertions(+), 27 deletions(-)

diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
index fceb146470..99eb08949b 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
@@ -1894,6 +1894,34 @@ QList<QVariant> Avatar::getSkeleton() {
     return QList<QVariant>();
 }
 
+void Avatar::updateJointMappings() {
+        {
+            QWriteLocker writeLock(&_jointDataLock);
+            _fstJointIndices.clear();
+            _fstJointNames.clear();
+            _jointData.clear();
+        }
+
+        //if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) {
+        //    ////
+        //    // TODO: Should we rely upon HTTPResourceRequest for ResourceRequestObserver instead?
+        //    // HTTPResourceRequest::doSend() covers all of the following and
+        //    // then some. It doesn't cover the connect() call, so we may
+        //    // want to add a HTTPResourceRequest::doSend() method that does
+        //    // connects.
+        //    QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
+        //    QNetworkRequest networkRequest = QNetworkRequest(_skeletonModelURL);
+        //    networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+        //    networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
+        //    DependencyManager::get<ResourceRequestObserver>()->update(
+        //        _skeletonModelURL, -1, "AvatarData::updateJointMappings");
+        //    QNetworkReply* networkReply = networkAccessManager.get(networkRequest);
+        //    //
+        //    ////
+        //    connect(networkReply, &QNetworkReply::finished, this, &AvatarData::setJointMappingsFromNetworkReply);
+        //}
+}
+
 void Avatar::addToScene(AvatarSharedPointer myHandle, const render::ScenePointer& scene) {
     if (scene) {
         auto nodelist = DependencyManager::get<NodeList>();
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
index 8f70b12122..d577ab35bf 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
@@ -516,6 +516,9 @@ protected:
     mutable QReadWriteLock _modelJointIndicesCacheLock;
     mutable bool _modelJointsCached { false };
 
+    /// Loads the joint indices, names from the FST file (if any)
+    virtual void updateJointMappings() override;
+
     glm::vec3 _skeletonOffset;
     std::vector<std::shared_ptr<Model>> _attachmentModels;
     std::vector<bool> _attachmentModelsTexturesLoaded;
diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index d9d4b57c31..a369ea9a24 100644
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -2209,33 +2209,8 @@ void AvatarData::sendIdentityPacket() {
     _identityDataChanged = false;
 }
 
-void AvatarData::updateJointMappings() {
-    {
-        QWriteLocker writeLock(&_jointDataLock);
-        _fstJointIndices.clear();
-        _fstJointNames.clear();
-        _jointData.clear();
-    }
-
-    if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) {
-        ////
-        // TODO: Should we rely upon HTTPResourceRequest for ResourceRequestObserver instead?
-        // HTTPResourceRequest::doSend() covers all of the following and
-        // then some. It doesn't cover the connect() call, so we may
-        // want to add a HTTPResourceRequest::doSend() method that does
-        // connects.
-        QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
-        QNetworkRequest networkRequest = QNetworkRequest(_skeletonModelURL);
-        networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
-        networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
-        DependencyManager::get<ResourceRequestObserver>()->update(
-            _skeletonModelURL, -1, "AvatarData::updateJointMappings");
-        QNetworkReply* networkReply = networkAccessManager.get(networkRequest);
-        //
-        ////
-        connect(networkReply, &QNetworkReply::finished, this, &AvatarData::setJointMappingsFromNetworkReply);
-    }
-}
+void AvatarData::updateJointMappings()
+{ }
 
 static const QString JSON_ATTACHMENT_URL = QStringLiteral("modelUrl");
 static const QString JSON_ATTACHMENT_JOINT_NAME = QStringLiteral("jointName");

From eb097af79617216c97806c59d3507e9ccd154d9d Mon Sep 17 00:00:00 2001
From: Simon Walton <simon@highfidelity.io>
Date: Mon, 3 Dec 2018 17:42:39 -0800
Subject: [PATCH 2/4] Remove all .fst downloads from AvatarData and its derived
 classes

---
 .../src/avatars/ScriptableAvatar.cpp          |  4 --
 .../src/avatars-renderer/Avatar.cpp           | 28 ----------
 .../src/avatars-renderer/Avatar.h             |  3 -
 libraries/avatars/src/AvatarData.cpp          | 56 -------------------
 libraries/avatars/src/AvatarData.h            |  9 ---
 5 files changed, 100 deletions(-)

diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp
index 51038a782f..392e9960e0 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.cpp
+++ b/assignment-client/src/avatars/ScriptableAvatar.cpp
@@ -77,10 +77,6 @@ static AnimPose composeAnimPose(const HFMJoint& joint, const glm::quat rotation,
 }
 
 void ScriptableAvatar::update(float deltatime) {
-    if (_bind.isNull() && !_skeletonFBXURL.isEmpty()) { // AvatarData will parse the .fst, but not get the .fbx skeleton.
-        _bind = DependencyManager::get<AnimationCache>()->getAnimation(_skeletonFBXURL);
-    }
-
     // Run animation
     if (_animation && _animation->isLoaded() && _animation->getFrames().size() > 0 && !_bind.isNull() && _bind->isLoaded()) {
         if (!_animSkeleton) {
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
index 99eb08949b..fceb146470 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
@@ -1894,34 +1894,6 @@ QList<QVariant> Avatar::getSkeleton() {
     return QList<QVariant>();
 }
 
-void Avatar::updateJointMappings() {
-        {
-            QWriteLocker writeLock(&_jointDataLock);
-            _fstJointIndices.clear();
-            _fstJointNames.clear();
-            _jointData.clear();
-        }
-
-        //if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) {
-        //    ////
-        //    // TODO: Should we rely upon HTTPResourceRequest for ResourceRequestObserver instead?
-        //    // HTTPResourceRequest::doSend() covers all of the following and
-        //    // then some. It doesn't cover the connect() call, so we may
-        //    // want to add a HTTPResourceRequest::doSend() method that does
-        //    // connects.
-        //    QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
-        //    QNetworkRequest networkRequest = QNetworkRequest(_skeletonModelURL);
-        //    networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
-        //    networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
-        //    DependencyManager::get<ResourceRequestObserver>()->update(
-        //        _skeletonModelURL, -1, "AvatarData::updateJointMappings");
-        //    QNetworkReply* networkReply = networkAccessManager.get(networkRequest);
-        //    //
-        //    ////
-        //    connect(networkReply, &QNetworkReply::finished, this, &AvatarData::setJointMappingsFromNetworkReply);
-        //}
-}
-
 void Avatar::addToScene(AvatarSharedPointer myHandle, const render::ScenePointer& scene) {
     if (scene) {
         auto nodelist = DependencyManager::get<NodeList>();
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
index d577ab35bf..8f70b12122 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
@@ -516,9 +516,6 @@ protected:
     mutable QReadWriteLock _modelJointIndicesCacheLock;
     mutable bool _modelJointsCached { false };
 
-    /// Loads the joint indices, names from the FST file (if any)
-    virtual void updateJointMappings() override;
-
     glm::vec3 _skeletonOffset;
     std::vector<std::shared_ptr<Model>> _attachmentModels;
     std::vector<bool> _attachmentModelsTexturesLoaded;
diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index a369ea9a24..420fa125e7 100644
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -2000,8 +2000,6 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) {
     
     _skeletonModelURL = expanded;
 
-    updateJointMappings();
-
     if (_clientTraitsHandler) {
         _clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonModelURL);
     }
@@ -2097,57 +2095,6 @@ void AvatarData::detachAll(const QString& modelURL, const QString& jointName) {
     setAttachmentData(attachmentData);
 }
 
-void AvatarData::setJointMappingsFromNetworkReply() {
-
-    QNetworkReply* networkReply = static_cast<QNetworkReply*>(sender());
-
-    // before we process this update, make sure that the skeleton model URL hasn't changed
-    // since we made the FST request
-    if (networkReply->url() != _skeletonModelURL) {
-        qCDebug(avatars) << "Refusing to set joint mappings for FST URL that does not match the current URL";
-        return;
-    }
-
-    {
-        QWriteLocker writeLock(&_jointDataLock);
-        QByteArray line;
-        while (!(line = networkReply->readLine()).isEmpty()) {
-            line = line.trimmed();
-            if (line.startsWith("filename")) {
-                int filenameIndex = line.indexOf('=') + 1;
-                    if (filenameIndex > 0) {
-                        _skeletonFBXURL = _skeletonModelURL.resolved(QString(line.mid(filenameIndex).trimmed()));
-                    }
-                }
-            if (!line.startsWith("jointIndex")) {
-                continue;
-            }
-            int jointNameIndex = line.indexOf('=') + 1;
-            if (jointNameIndex == 0) {
-                continue;
-            }
-            int secondSeparatorIndex = line.indexOf('=', jointNameIndex);
-            if (secondSeparatorIndex == -1) {
-                continue;
-            }
-            QString jointName = line.mid(jointNameIndex, secondSeparatorIndex - jointNameIndex).trimmed();
-            bool ok;
-            int jointIndex = line.mid(secondSeparatorIndex + 1).trimmed().toInt(&ok);
-            if (ok) {
-                while (_fstJointNames.size() < jointIndex + 1) {
-                    _fstJointNames.append(QString());
-                }
-                _fstJointNames[jointIndex] = jointName;
-            }
-        }
-        for (int i = 0; i < _fstJointNames.size(); i++) {
-            _fstJointIndices.insert(_fstJointNames.at(i), i + 1);
-        }
-    }
-
-    networkReply->deleteLater();
-}
-
 void AvatarData::sendAvatarDataPacket(bool sendAll) {
     auto nodeList = DependencyManager::get<NodeList>();
 
@@ -2209,9 +2156,6 @@ void AvatarData::sendIdentityPacket() {
     _identityDataChanged = false;
 }
 
-void AvatarData::updateJointMappings()
-{ }
-
 static const QString JSON_ATTACHMENT_URL = QStringLiteral("modelUrl");
 static const QString JSON_ATTACHMENT_JOINT_NAME = QStringLiteral("jointName");
 static const QString JSON_ATTACHMENT_TRANSFORM = QStringLiteral("transform");
diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h
index 36c6ed6c50..c3819d0d28 100644
--- a/libraries/avatars/src/AvatarData.h
+++ b/libraries/avatars/src/AvatarData.h
@@ -1269,11 +1269,6 @@ public slots:
      */
     void sendIdentityPacket();
 
-    /**jsdoc
-     * @function MyAvatar.setJointMappingsFromNetworkReply
-     */
-    void setJointMappingsFromNetworkReply();
-
     /**jsdoc
      * @function MyAvatar.setSessionUUID
      * @param {Uuid} sessionUUID
@@ -1376,7 +1371,6 @@ protected:
     mutable HeadData* _headData { nullptr };
 
     QUrl _skeletonModelURL;
-    QUrl _skeletonFBXURL;
     QVector<AttachmentData> _attachmentData;
     QVector<AttachmentData> _oldAttachmentData;
     QString _displayName;
@@ -1390,9 +1384,6 @@ protected:
 
     QWeakPointer<Node> _owningAvatarMixer;
 
-    /// Loads the joint indices, names from the FST file (if any)
-    virtual void updateJointMappings();
-
     glm::vec3 _targetVelocity;
 
     SimpleMovingAverage _averageBytesReceived;

From f3236e0843299552dae90e535712088a36b81f3e Mon Sep 17 00:00:00 2001
From: Simon Walton <simon@highfidelity.io>
Date: Tue, 4 Dec 2018 10:45:32 -0800
Subject: [PATCH 3/4] Remove FST joint members from AvatarData

---
 libraries/avatars/src/AvatarData.cpp |  9 ++-------
 libraries/avatars/src/AvatarData.h   | 11 +----------
 2 files changed, 3 insertions(+), 17 deletions(-)

diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index 420fa125e7..8705c35630 100644
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -1776,16 +1776,11 @@ int AvatarData::getFauxJointIndex(const QString& name) const {
 
 int AvatarData::getJointIndex(const QString& name) const {
     int result = getFauxJointIndex(name);
-    if (result != -1) {
-        return result;
-    }
-    QReadLocker readLock(&_jointDataLock);
-    return _fstJointIndices.value(name) - 1;
+    return result;
 }
 
 QStringList AvatarData::getJointNames() const {
-    QReadLocker readLock(&_jointDataLock);
-    return _fstJointNames;
+    return QStringList();
 }
 
 glm::quat AvatarData::getOrientationOutbound() const {
diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h
index c3819d0d28..49fd49df58 100644
--- a/libraries/avatars/src/AvatarData.h
+++ b/libraries/avatars/src/AvatarData.h
@@ -1377,9 +1377,6 @@ protected:
     QString _sessionDisplayName { };
     bool _lookAtSnappingEnabled { true };
 
-    QHash<QString, int> _fstJointIndices; ///< 1-based, since zero is returned for missing keys
-    QStringList _fstJointNames; ///< in order of depth-first traversal
-
     quint64 _errorLogExpiry; ///< time in future when to log an error
 
     QWeakPointer<Node> _owningAvatarMixer;
@@ -1487,11 +1484,8 @@ protected:
     T readLockWithNamedJointIndex(const QString& name, const T& defaultValue, F f) const {
         int index = getFauxJointIndex(name);
         QReadLocker readLock(&_jointDataLock);
-        if (index == -1) {
-            index = _fstJointIndices.value(name) - 1;
-        }
 
-        // The first conditional is superfluous, but illsutrative
+        // The first conditional is superfluous, but illustrative
         if (index == -1 || index < _jointData.size()) {
             return defaultValue;
         }
@@ -1508,9 +1502,6 @@ protected:
     void writeLockWithNamedJointIndex(const QString& name, F f) {
         int index = getFauxJointIndex(name);
         QWriteLocker writeLock(&_jointDataLock);
-        if (index == -1) {
-            index = _fstJointIndices.value(name) - 1;
-        }
         if (index == -1) {
             return;
         }

From 16144b663036cbce530f77091a4ab8217f22450f Mon Sep 17 00:00:00 2001
From: Simon Walton <simon@highfidelity.io>
Date: Wed, 5 Dec 2018 17:14:40 -0800
Subject: [PATCH 4/4] Move the previously deleted FST reader down to the
 ScriptableAvatar class

---
 .../src/avatars/ScriptableAvatar.cpp          | 96 +++++++++++++++++++
 .../src/avatars/ScriptableAvatar.h            | 32 +++++++
 2 files changed, 128 insertions(+)

diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp
index 392e9960e0..29a66b44fc 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.cpp
+++ b/assignment-client/src/avatars/ScriptableAvatar.cpp
@@ -19,6 +19,9 @@
 #include <AnimUtil.h>
 #include <ClientTraitsHandler.h>
 #include <GLMHelpers.h>
+#include <ResourceRequestObserver.h>
+#include <AvatarLogging.h>
+
 
 ScriptableAvatar::ScriptableAvatar() {
     _clientTraitsHandler = std::unique_ptr<ClientTraitsHandler>(new ClientTraitsHandler(this));
@@ -62,11 +65,28 @@ AnimationDetails ScriptableAvatar::getAnimationDetails() {
     return _animationDetails;
 }
 
+int ScriptableAvatar::getJointIndex(const QString& name) const {
+    // Faux joints:
+    int result = AvatarData::getJointIndex(name);
+    if (result != -1) {
+        return result;
+    }
+    QReadLocker readLock(&_jointDataLock);
+    return _fstJointIndices.value(name) - 1;
+}
+
+QStringList ScriptableAvatar::getJointNames() const {
+    QReadLocker readLock(&_jointDataLock);
+    return _fstJointNames;
+    return QStringList();
+}
+
 void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
     _bind.reset();
     _animSkeleton.reset();
 
     AvatarData::setSkeletonModelURL(skeletonModelURL);
+    updateJointMappings();
 }
 
 static AnimPose composeAnimPose(const HFMJoint& joint, const glm::quat rotation, const glm::vec3 translation) {
@@ -142,6 +162,82 @@ void ScriptableAvatar::update(float deltatime) {
     _clientTraitsHandler->sendChangedTraitsToMixer();
 }
 
+void ScriptableAvatar::updateJointMappings() {
+    {
+        QWriteLocker writeLock(&_jointDataLock);
+        _fstJointIndices.clear();
+        _fstJointNames.clear();
+        _jointData.clear();
+    }
+
+    if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) {
+        ////
+        // TODO: Should we rely upon HTTPResourceRequest for ResourceRequestObserver instead?
+        // HTTPResourceRequest::doSend() covers all of the following and
+        // then some. It doesn't cover the connect() call, so we may
+        // want to add a HTTPResourceRequest::doSend() method that does
+        // connects.
+        QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
+        QNetworkRequest networkRequest = QNetworkRequest(_skeletonModelURL);
+        networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+        networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
+        DependencyManager::get<ResourceRequestObserver>()->update(
+            _skeletonModelURL, -1, "AvatarData::updateJointMappings");
+        QNetworkReply* networkReply = networkAccessManager.get(networkRequest);
+        //
+        ////
+        connect(networkReply, &QNetworkReply::finished, this, &ScriptableAvatar::setJointMappingsFromNetworkReply);
+    }
+}
+
+void ScriptableAvatar::setJointMappingsFromNetworkReply() {
+    QNetworkReply* networkReply = static_cast<QNetworkReply*>(sender());
+    // before we process this update, make sure that the skeleton model URL hasn't changed
+    // since we made the FST request
+    if (networkReply->url() != _skeletonModelURL) {
+        qCDebug(avatars) << "Refusing to set joint mappings for FST URL that does not match the current URL";
+        networkReply->deleteLater();
+        return;
+    }
+    {
+        QWriteLocker writeLock(&_jointDataLock);
+        QByteArray line;
+        while (!(line = networkReply->readLine()).isEmpty()) {
+            line = line.trimmed();
+            if (line.startsWith("filename")) {
+                int filenameIndex = line.indexOf('=') + 1;
+                if (filenameIndex > 0) {
+                    _skeletonFBXURL = _skeletonModelURL.resolved(QString(line.mid(filenameIndex).trimmed()));
+                }
+            }
+            if (!line.startsWith("jointIndex")) {
+                continue;
+            }
+            int jointNameIndex = line.indexOf('=') + 1;
+            if (jointNameIndex == 0) {
+                continue;
+            }
+            int secondSeparatorIndex = line.indexOf('=', jointNameIndex);
+            if (secondSeparatorIndex == -1) {
+                continue;
+            }
+            QString jointName = line.mid(jointNameIndex, secondSeparatorIndex - jointNameIndex).trimmed();
+            bool ok;
+            int jointIndex = line.mid(secondSeparatorIndex + 1).trimmed().toInt(&ok);
+            if (ok) {
+                while (_fstJointNames.size() < jointIndex + 1) {
+                    _fstJointNames.append(QString());
+                }
+                _fstJointNames[jointIndex] = jointName;
+            }
+        }
+        for (int i = 0; i < _fstJointNames.size(); i++) {
+            _fstJointIndices.insert(_fstJointNames.at(i), i + 1);
+        }
+    }
+    networkReply->deleteLater();
+}
+
 void ScriptableAvatar::setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement) {
     _headData->setHasProceduralBlinkFaceMovement(hasProceduralBlinkFaceMovement);
 }
diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h
index 578bd84a8f..66b0b5ae3f 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.h
+++ b/assignment-client/src/avatars/ScriptableAvatar.h
@@ -153,6 +153,27 @@ public:
      */
     Q_INVOKABLE AnimationDetails getAnimationDetails();
 
+    /**jsdoc
+    * Get the names of all the joints in the current avatar.
+    * @function MyAvatar.getJointNames
+    * @returns {string[]} The joint names.
+    * @example <caption>Report the names of all the joints in your current avatar.</caption>
+    * print(JSON.stringify(MyAvatar.getJointNames()));
+    */
+    Q_INVOKABLE virtual QStringList getJointNames() const override;
+
+    /**jsdoc
+    * Get the joint index for a named joint. The joint index value is the position of the joint in the array returned by
+    * {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}.
+    * @function MyAvatar.getJointIndex
+    * @param {string} name - The name of the joint.
+    * @returns {number} The index of the joint.
+    * @example <caption>Report the index of your avatar's left arm joint.</caption>
+    * print(JSON.stringify(MyAvatar.getJointIndex("LeftArm"));
+    */
+    /// Returns the index of the joint with the specified name, or -1 if not found/unknown.
+    Q_INVOKABLE virtual int getJointIndex(const QString& name) const override;
+
     virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override;
 
     virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override;
@@ -167,12 +188,23 @@ public:
 public slots:
     void update(float deltatime);
 
+    /**jsdoc
+    * @function MyAvatar.setJointMappingsFromNetworkReply
+    */
+    void setJointMappingsFromNetworkReply();
+
 private:
     AnimationPointer _animation;
     AnimationDetails _animationDetails;
     QStringList _maskedJoints;
     AnimationPointer _bind; // a sleazy way to get the skeleton, given the various library/cmake dependencies
     std::shared_ptr<AnimSkeleton> _animSkeleton;
+    QHash<QString, int> _fstJointIndices; ///< 1-based, since zero is returned for missing keys
+    QStringList _fstJointNames; ///< in order of depth-first traversal
+    QUrl _skeletonFBXURL;
+
+    /// Loads the joint indices, names from the FST file (if any)
+    void updateJointMappings();
 };
 
 #endif // hifi_ScriptableAvatar_h