diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs
index b6470a7551..8f4d5a7962 100644
--- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs
+++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs
@@ -12,7 +12,43 @@ using System;
 using System.IO;
 using System.Collections.Generic;
 
-class AvatarExporter : MonoBehaviour {    
+class AvatarExporter : MonoBehaviour {
+    // update version number for every PR that changes this file, also set updated version in README file
+    static readonly string AVATAR_EXPORTER_VERSION = "0.1";
+    
+    static readonly float HIPS_GROUND_MIN_Y = 0.01f;
+    static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f;
+    static readonly int MAXIMUM_USER_BONE_COUNT = 256;
+    static readonly string EMPTY_WARNING_TEXT = "None";
+    
+    static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] {
+        "2018.2.12f1",
+        "2018.2.11f1",
+        "2018.2.10f1",
+        "2018.2.9f1",
+        "2018.2.8f1",
+        "2018.2.7f1",
+        "2018.2.6f1",
+        "2018.2.5f1",
+        "2018.2.4f1",
+        "2018.2.3f1",
+        "2018.2.2f1",
+        "2018.2.1f1",
+        "2018.2.0f2",
+        "2018.1.9f2",
+        "2018.1.8f1",
+        "2018.1.7f1",
+        "2018.1.6f1",
+        "2018.1.5f1",
+        "2018.1.4f1",
+        "2018.1.3f1",
+        "2018.1.2f1",
+        "2018.1.1f1",
+        "2018.1.0f2",
+        "2017.4.18f1",
+        "2017.4.17f1",
+    };
+   
     static readonly Dictionary<string, string> HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary<string, string> {
         {"Chest", "Spine1"},
         {"Head", "Head"},
@@ -70,70 +106,163 @@ class AvatarExporter : MonoBehaviour {
         {"UpperChest", "Spine2"},
     };
     
-    static readonly Dictionary<string, Quaternion> referenceAbsoluteRotations = new Dictionary<string, Quaternion> {
+    // absolute reference rotations for each Humanoid bone using Artemis fbx in Unity 2018.2.12f1
+    static readonly Dictionary<string, Quaternion> REFERENCE_ROTATIONS = new Dictionary<string, Quaternion> {
+        {"Chest", new Quaternion(-0.0824653f, 1.25274e-7f, -6.75759e-6f, 0.996594f)},
         {"Head", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
         {"Hips", new Quaternion(-3.043941e-10f, -1.573706e-7f, 5.112975e-6f, 1f)},
-        {"LeftHandIndex3", new Quaternion(-0.5086057f, 0.4908088f, -0.4912299f, -0.5090388f)},
-        {"LeftHandIndex2", new Quaternion(-0.4934928f, 0.5062312f, -0.5064303f, -0.4936835f)},
-        {"LeftHandIndex1", new Quaternion(-0.4986293f, 0.5017503f, -0.5013659f, -0.4982448f)},
-        {"LeftHandPinky3", new Quaternion(-0.490056f, 0.5143053f, -0.5095307f, -0.4855038f)},
-        {"LeftHandPinky2", new Quaternion(-0.5083722f, 0.4954255f, -0.4915887f, -0.5044324f)},
-        {"LeftHandPinky1", new Quaternion(-0.5062528f, 0.497324f, -0.4937346f, -0.5025966f)},
-        {"LeftHandMiddle3", new Quaternion(-0.4871885f, 0.5123404f, -0.5125002f, -0.4873383f)},
-        {"LeftHandMiddle2", new Quaternion(-0.5171652f, 0.4827828f, -0.4822642f, -0.5166069f)},
-        {"LeftHandMiddle1", new Quaternion(-0.4955998f, 0.5041052f, -0.5043675f, -0.4958555f)},
-        {"LeftHandRing3", new Quaternion(-0.4936301f, 0.5097645f, -0.5061787f, -0.4901562f)},
-        {"LeftHandRing2", new Quaternion(-0.5089865f, 0.4943658f, -0.4909532f, -0.5054707f)},
-        {"LeftHandRing1", new Quaternion(-0.5020972f, 0.5005084f, -0.4979034f, -0.4994819f)},
-        {"LeftHandThumb3", new Quaternion(-0.6617184f, 0.2884935f, -0.3604706f, -0.5907297f)},
-        {"LeftHandThumb2", new Quaternion(-0.6935627f, 0.1995147f, -0.2805665f, -0.6328092f)},
-        {"LeftHandThumb1", new Quaternion(-0.6663674f, 0.278572f, -0.3507071f, -0.5961183f)},
+        {"Left Index Distal", new Quaternion(-0.5086057f, 0.4908088f, -0.4912299f, -0.5090388f)},
+        {"Left Index Intermediate", new Quaternion(-0.4934928f, 0.5062312f, -0.5064303f, -0.4936835f)},
+        {"Left Index Proximal", new Quaternion(-0.4986293f, 0.5017503f, -0.5013659f, -0.4982448f)},
+        {"Left Little Distal", new Quaternion(-0.490056f, 0.5143053f, -0.5095307f, -0.4855038f)},
+        {"Left Little Intermediate", new Quaternion(-0.5083722f, 0.4954255f, -0.4915887f, -0.5044324f)},
+        {"Left Little Proximal", new Quaternion(-0.5062528f, 0.497324f, -0.4937346f, -0.5025966f)},
+        {"Left Middle Distal", new Quaternion(-0.4871885f, 0.5123404f, -0.5125002f, -0.4873383f)},
+        {"Left Middle Intermediate", new Quaternion(-0.5171652f, 0.4827828f, -0.4822642f, -0.5166069f)},
+        {"Left Middle Proximal", new Quaternion(-0.4955998f, 0.5041052f, -0.5043675f, -0.4958555f)},
+        {"Left Ring Distal", new Quaternion(-0.4936301f, 0.5097645f, -0.5061787f, -0.4901562f)},
+        {"Left Ring Intermediate", new Quaternion(-0.5089865f, 0.4943658f, -0.4909532f, -0.5054707f)},
+        {"Left Ring Proximal", new Quaternion(-0.5020972f, 0.5005084f, -0.4979034f, -0.4994819f)},
+        {"Left Thumb Distal", new Quaternion(-0.6617184f, 0.2884935f, -0.3604706f, -0.5907297f)},
+        {"Left Thumb Intermediate", new Quaternion(-0.6935627f, 0.1995147f, -0.2805665f, -0.6328092f)},
+        {"Left Thumb Proximal", new Quaternion(-0.6663674f, 0.278572f, -0.3507071f, -0.5961183f)},
         {"LeftEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
         {"LeftFoot", new Quaternion(0.009215056f, 0.3612514f, 0.9323555f, -0.01121602f)},
         {"LeftHand", new Quaternion(-0.4797408f, 0.5195366f, -0.5279632f, -0.4703038f)},
-        {"LeftForeArm", new Quaternion(-0.4594738f, 0.4594729f, -0.5374805f, -0.5374788f)},
-        {"LeftLeg", new Quaternion(-0.0005380471f, -0.03154583f, 0.9994993f, 0.002378627f)},
+        {"LeftLowerArm", new Quaternion(-0.4594738f, 0.4594729f, -0.5374805f, -0.5374788f)},
+        {"LeftLowerLeg", new Quaternion(-0.0005380471f, -0.03154583f, 0.9994993f, 0.002378627f)},
         {"LeftShoulder", new Quaternion(-0.3840606f, 0.525857f, -0.5957767f, -0.47013f)},
-        {"LeftToeBase", new Quaternion(-0.0002536641f, 0.7113448f, 0.7027079f, -0.01379319f)},
-        {"LeftArm", new Quaternion(-0.4591927f, 0.4591916f, -0.5377204f, -0.5377193f)},
-        {"LeftUpLeg", new Quaternion(-0.0006682819f, 0.0006864658f, 0.9999968f, -0.002333928f)},
+        {"LeftToes", new Quaternion(-0.0002536641f, 0.7113448f, 0.7027079f, -0.01379319f)},
+        {"LeftUpperArm", new Quaternion(-0.4591927f, 0.4591916f, -0.5377204f, -0.5377193f)},
+        {"LeftUpperLeg", new Quaternion(-0.0006682819f, 0.0006864658f, 0.9999968f, -0.002333928f)},
         {"Neck", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
-        {"RightHandIndex3", new Quaternion(0.5083892f, 0.4911618f, -0.4914584f, 0.5086939f)},
-        {"RightHandIndex2", new Quaternion(0.4931984f, 0.5065879f, -0.5067145f, 0.4933202f)},
-        {"RightHandIndex1", new Quaternion(0.4991491f, 0.5012957f, -0.5008481f, 0.4987026f)},
-        {"RightHandPinky3", new Quaternion(0.4890696f, 0.5154139f, -0.5104482f, 0.4843578f)},
-        {"RightHandPinky2", new Quaternion(0.5084175f, 0.495413f, -0.4915423f, 0.5044444f)},
-        {"RightHandPinky1", new Quaternion(0.5069782f, 0.4965974f, -0.4930001f, 0.5033045f)},
-        {"RightHandMiddle3", new Quaternion(0.4867662f, 0.5129694f, -0.5128888f, 0.4866894f)},
-        {"RightHandMiddle2", new Quaternion(0.5167004f, 0.4833596f, -0.4827653f, 0.5160643f)},
-        {"RightHandMiddle1", new Quaternion(0.4965845f, 0.5031784f, -0.5033959f, 0.4967981f)},
-        {"RightHandRing3", new Quaternion(0.4933217f, 0.5102056f, -0.5064691f, 0.4897075f)},
-        {"RightHandRing2", new Quaternion(0.5085972f, 0.494844f, -0.4913519f, 0.505007f)},
-        {"RightHandRing1", new Quaternion(0.502959f, 0.4996676f, -0.4970418f, 0.5003144f)},
-        {"RightHandThumb3", new Quaternion(0.6611374f, 0.2896575f, -0.3616535f, 0.5900872f)},
-        {"RightHandThumb2", new Quaternion(0.6937408f, 0.1986776f, -0.279922f, 0.6331626f)},
-        {"RightHandThumb1", new Quaternion(0.6664271f, 0.2783172f, -0.3505667f, 0.596253f)},
+        {"Right Index Distal", new Quaternion(0.5083892f, 0.4911618f, -0.4914584f, 0.5086939f)},
+        {"Right Index Intermediate", new Quaternion(0.4931984f, 0.5065879f, -0.5067145f, 0.4933202f)},
+        {"Right Index Proximal", new Quaternion(0.4991491f, 0.5012957f, -0.5008481f, 0.4987026f)},
+        {"Right Little Distal", new Quaternion(0.4890696f, 0.5154139f, -0.5104482f, 0.4843578f)},
+        {"Right Little Intermediate", new Quaternion(0.5084175f, 0.495413f, -0.4915423f, 0.5044444f)},
+        {"Right Little Proximal", new Quaternion(0.5069782f, 0.4965974f, -0.4930001f, 0.5033045f)},
+        {"Right Middle Distal", new Quaternion(0.4867662f, 0.5129694f, -0.5128888f, 0.4866894f)},
+        {"Right Middle Intermediate", new Quaternion(0.5167004f, 0.4833596f, -0.4827653f, 0.5160643f)},
+        {"Right Middle Proximal", new Quaternion(0.4965845f, 0.5031784f, -0.5033959f, 0.4967981f)},
+        {"Right Ring Distal", new Quaternion(0.4933217f, 0.5102056f, -0.5064691f, 0.4897075f)},
+        {"Right Ring Intermediate", new Quaternion(0.5085972f, 0.494844f, -0.4913519f, 0.505007f)},
+        {"Right Ring Proximal", new Quaternion(0.502959f, 0.4996676f, -0.4970418f, 0.5003144f)},
+        {"Right Thumb Distal", new Quaternion(0.6611374f, 0.2896575f, -0.3616535f, 0.5900872f)},
+        {"Right Thumb Intermediate", new Quaternion(0.6937408f, 0.1986776f, -0.279922f, 0.6331626f)},
+        {"Right Thumb Proximal", new Quaternion(0.6664271f, 0.2783172f, -0.3505667f, 0.596253f)},
         {"RightEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)},
         {"RightFoot", new Quaternion(-0.009482829f, 0.3612484f, 0.9323512f, 0.01144584f)},
         {"RightHand", new Quaternion(0.4797273f, 0.5195542f, -0.5279628f, 0.4702987f)},
-        {"RightForeArm", new Quaternion(0.4594217f, 0.4594215f, -0.5375242f, 0.5375237f)},
-        {"RightLeg", new Quaternion(0.0005446263f, -0.03177159f, 0.9994922f, -0.002395923f)},
+        {"RightLowerArm", new Quaternion(0.4594217f, 0.4594215f, -0.5375242f, 0.5375237f)},
+        {"RightLowerLeg", new Quaternion(0.0005446263f, -0.03177159f, 0.9994922f, -0.002395923f)},
         {"RightShoulder", new Quaternion(0.3841222f, 0.5257177f, -0.5957286f, 0.4702966f)},
-        {"RightToeBase", new Quaternion(0.0001034f, 0.7113398f, 0.7027067f, 0.01411251f)},
-        {"RightArm", new Quaternion(0.4591419f, 0.4591423f, -0.537763f, 0.5377624f)},
-        {"RightUpLeg", new Quaternion(0.0006750703f, 0.0008973633f, 0.9999966f, 0.002352045f)},
+        {"RightToes", new Quaternion(0.0001034f, 0.7113398f, 0.7027067f, 0.01411251f)},
+        {"RightUpperArm", new Quaternion(0.4591419f, 0.4591423f, -0.537763f, 0.5377624f)},
+        {"RightUpperLeg", new Quaternion(0.0006750703f, 0.0008973633f, 0.9999966f, 0.002352045f)},
         {"Spine", new Quaternion(-0.05427956f, 1.508558e-7f, -2.775203e-6f, 0.9985258f)},
-        {"Spine1", new Quaternion(-0.0824653f, 1.25274e-7f, -6.75759e-6f, 0.996594f)},
-        {"Spine2", new Quaternion(-0.0824653f, 1.25274e-7f, -6.75759e-6f, 0.996594f)},
+        {"UpperChest", new Quaternion(-0.0824653f, 1.25274e-7f, -6.75759e-6f, 0.996594f)},
+    };
+    
+    // Humanoid mapping name suffixes for each set of appendages
+    static readonly string[] LEG_MAPPING_SUFFIXES = new string[] {
+        "UpperLeg",
+        "LowerLeg",
+        "Foot",
+        "Toes",
+    };
+    static readonly string[] ARM_MAPPING_SUFFIXES = new string[] {
+        "Shoulder",
+        "UpperArm",
+        "LowerArm",
+        "Hand",
+    };
+    static readonly string[] HAND_MAPPING_SUFFIXES = new string[] {
+        " Index Distal",
+        " Index Intermediate",
+        " Index Proximal",
+        " Little Distal",
+        " Little Intermediate",
+        " Little Proximal",
+        " Middle Distal",
+        " Middle Intermediate",
+        " Middle Proximal",
+        " Ring Distal",
+        " Ring Intermediate",
+        " Ring Proximal",
+        " Thumb Distal",
+        " Thumb Intermediate",
+        " Thumb Proximal",
     };
 
-    static Dictionary<string, string> userBoneToHumanoidMappings = new Dictionary<string, string>();
-    static Dictionary<string, string> userParentNames = new Dictionary<string, string>();
-    static Dictionary<string, Quaternion> userAbsoluteRotations = new Dictionary<string, Quaternion>();
+    enum BoneRule {
+        RecommendedUnityVersion,
+        SingleRoot,
+        NoDuplicateMapping,
+        NoAsymmetricalLegMapping,
+        NoAsymmetricalArmMapping,
+        NoAsymmetricalHandMapping,
+        HipsMapped,
+        SpineMapped,
+        SpineDescendantOfHips,
+        ChestMapped,
+        ChestDescendantOfSpine,
+        NeckMapped,
+        HeadMapped,
+        HeadDescendantOfChest,
+        EyesMapped,
+        HipsNotOnGround,
+        HipsSpineChestNotCoincident,
+        TotalBoneCountUnderLimit,
+        BoneRuleEnd,
+    };
+    // rules that are treated as errors and prevent exporting, otherwise rules will show as warnings
+    static readonly BoneRule[] EXPORT_BLOCKING_BONE_RULES = new BoneRule[] {
+        BoneRule.HipsMapped,
+        BoneRule.SpineMapped,
+        BoneRule.ChestMapped,
+        BoneRule.HeadMapped,
+    };
+    
+    class UserBoneInformation {
+        public string humanName; // bone name in Humanoid if it is mapped, otherwise ""
+        public string parentName; // parent user bone name
+        public int mappingCount; // number of times this bone is mapped in Humanoid
+        public Vector3 position; // absolute position
+        public Quaternion rotation; // absolute rotation
+        public BoneTreeNode boneTreeNode;
+        
+        public UserBoneInformation() {
+            humanName = "";
+            parentName = "";
+            mappingCount = 0;
+            position = new Vector3();
+            rotation = new Quaternion();
+            boneTreeNode = new BoneTreeNode();
+        }
+        
+        public bool HasHumanMapping() { return !string.IsNullOrEmpty(humanName); }
+    }
+    
+    class BoneTreeNode { 
+        public string boneName;
+        public List<BoneTreeNode> children = new List<BoneTreeNode>();
+        
+        public BoneTreeNode() {}
+        public BoneTreeNode(string name) {
+            boneName = name;
+        }
+    }
+    
+    static Dictionary<string, UserBoneInformation> userBoneInfos = new Dictionary<string, UserBoneInformation>();
+    static Dictionary<string, string> humanoidToUserBoneMappings = new Dictionary<string, string>();
+    static BoneTreeNode userBoneTree = new BoneTreeNode();
+    static Dictionary<BoneRule, string> failedBoneRules = new Dictionary<BoneRule, string>();
     
     static string assetPath = "";
     static string assetName = "";
     static HumanDescription humanDescription;
+    
  
     [MenuItem("High Fidelity/Export New Avatar")]
     static void ExportNewAvatar() {
@@ -144,6 +273,11 @@ class AvatarExporter : MonoBehaviour {
     static void UpdateAvatar() {
         ExportSelectedAvatar(true);
     }
+    
+    [MenuItem("High Fidelity/About")]
+    static void About() {
+        EditorUtility.DisplayDialog("About", "High Fidelity, Inc.\nAvatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION, "Ok");
+    }
 
     static void ExportSelectedAvatar(bool updateAvatar) {     
         string[] guids = Selection.assetGUIDs;
@@ -163,14 +297,58 @@ class AvatarExporter : MonoBehaviour {
             return;
         }
         if (modelImporter.animationType != ModelImporterAnimationType.Human) {
-            EditorUtility.DisplayDialog("Error", "Please set model's Animation Type to Humanoid in the Rig section of it's Inspector window.", "Ok");
+            EditorUtility.DisplayDialog("Error", "Please set model's Animation Type to Humanoid in " + 
+                                                 " the Rig section of it's Inspector window.", "Ok");
             return;
         }
-                
-        humanDescription = modelImporter.humanDescription;   
-        if (!SetJointMappingsAndParentNames()) {
+        
+        humanDescription = modelImporter.humanDescription;
+        SetUserBoneInformation();
+
+        // format resulting bone rule failure strings
+        // consider export-blocking bone rules to be errors and show them in an error dialog,
+        // and also include any other bone rule failures as warnings in the dialog
+        string boneErrors = "";
+        string boneWarnings = "";
+        foreach (var failedBoneRule in failedBoneRules) {
+            if (Array.IndexOf(EXPORT_BLOCKING_BONE_RULES, failedBoneRule.Key) >= 0) {
+                boneErrors += failedBoneRule.Value + "\n\n";
+            } else {
+                boneWarnings += failedBoneRule.Value + "\n\n";
+            }           
+        }
+        if (!string.IsNullOrEmpty(boneErrors)) {
+            // if there are both errors and warnings then warnings will be displayed with errors in the error dialog
+            if (!string.IsNullOrEmpty(boneWarnings)) {
+                boneErrors = "Errors:\n\n" + boneErrors;
+                boneErrors += "Warnings:\n\n" + boneWarnings;
+            }
+            // remove ending newlines from the last rule failure string that was added above
+            boneErrors = boneErrors.Substring(0, boneErrors.LastIndexOf("\n\n"));
+            EditorUtility.DisplayDialog("Error", boneErrors, "Ok");
             return;
         }
+        
+        if (!humanoidToUserBoneMappings.ContainsKey("UpperChest")) {
+            // if parent of Neck is not Chest then map the parent to UpperChest
+            string neckUserBone;
+            if (humanoidToUserBoneMappings.TryGetValue("Neck", out neckUserBone)) {
+                UserBoneInformation neckParentBoneInfo;
+                string neckParentUserBone = userBoneInfos[neckUserBone].parentName;
+                if (userBoneInfos.TryGetValue(neckParentUserBone, out neckParentBoneInfo) && !neckParentBoneInfo.HasHumanMapping()) {
+                    neckParentBoneInfo.humanName = "UpperChest";
+                    humanoidToUserBoneMappings.Add("UpperChest", neckParentUserBone);
+                }
+            }
+            // if there is still no UpperChest bone but there is a Chest bone then we remap Chest to UpperChest 
+            string chestUserBone;
+            if (!humanoidToUserBoneMappings.ContainsKey("UpperChest") && 
+                humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) {       
+                userBoneInfos[chestUserBone].humanName = "UpperChest";
+                humanoidToUserBoneMappings.Remove("Chest");
+                humanoidToUserBoneMappings.Add("UpperChest", chestUserBone);
+            }
+        }
 
         string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
         string hifiFolder = documentsFolder + "\\High Fidelity Projects";
@@ -236,11 +414,12 @@ class AvatarExporter : MonoBehaviour {
                         modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter;
                         modelImporter.animationType = ModelImporterAnimationType.Human;
                         EditorUtility.SetDirty(modelImporter);
-                        modelImporter.SaveAndReimport();
-                        humanDescription = modelImporter.humanDescription;   
+                        modelImporter.SaveAndReimport(); 
                         
-                        // redo joint mappings and parent names due to the fbx change
-                        SetJointMappingsAndParentNames();
+                        // redo parent names, joint mappings, and user bone positions due to the fbx change
+                        // as well as re-check the bone rules for failures
+                        humanDescription = modelImporter.humanDescription;
+                        SetUserBoneInformation();
                     }
                 }
             } else {
@@ -277,19 +456,30 @@ class AvatarExporter : MonoBehaviour {
 
             // write out a new fst file in place of the old file
             WriteFST(exportFstPath, projectName);
+            
+            // display success dialog with any bone rule warnings
+            string successDialog = "Avatar successfully updated!";
+            if (!string.IsNullOrEmpty(boneWarnings)) {
+                successDialog += "\n\nWarnings:\n" + boneWarnings;
+            }
+            EditorUtility.DisplayDialog("Success!", successDialog, "Ok");
         } else { // Export New Avatar menu option
             // create High Fidelity Projects folder in user documents folder if it doesn't exist
             if (!Directory.Exists(hifiFolder)) {    
                 Directory.CreateDirectory(hifiFolder);
             }
             
+            if (string.IsNullOrEmpty(boneWarnings)) {
+                boneWarnings = EMPTY_WARNING_TEXT;
+            }
+            
             // open a popup window to enter new export project name and project location
             ExportProjectWindow window = ScriptableObject.CreateInstance<ExportProjectWindow>();
-            window.Init(hifiFolder, OnExportProjectWindowClose);
+            window.Init(hifiFolder, boneWarnings, OnExportProjectWindowClose);
         }
     }
     
-    static void OnExportProjectWindowClose(string projectDirectory, string projectName) {
+    static void OnExportProjectWindowClose(string projectDirectory, string projectName, string warnings) {
         // copy the fbx from the Unity Assets folder to the project directory
         string exportModelPath = projectDirectory + assetName + ".fbx";
         File.Copy(assetPath, exportModelPath);
@@ -304,94 +494,19 @@ class AvatarExporter : MonoBehaviour {
         string exportFstPath = projectDirectory + "avatar.fst";
         WriteFST(exportFstPath, projectName);
         
-        // remove any double slashes in texture directory path and warn user to copy external textures over
+        // remove any double slashes in texture directory path, display success dialog with any
+        // bone warnings previously mentioned, and suggest user to copy external textures over
         texturesDirectory = texturesDirectory.Replace("\\\\", "\\");
-        EditorUtility.DisplayDialog("Warning", "If you are using any external textures with your model, " +
-                                               "please copy those textures to " + texturesDirectory, "Ok");
+        string successDialog = "Avatar successfully exported!\n\n";
+        if (warnings != EMPTY_WARNING_TEXT) {
+            successDialog += "Warnings:\n" + warnings;
+        }
+        successDialog += "Note: If you are using any external textures with your model, " +
+                         "please copy those textures to " + texturesDirectory;
+        EditorUtility.DisplayDialog("Success!", successDialog, "Ok");
     }
     
-    static bool SetJointMappingsAndParentNames() {
-        userParentNames.Clear();
-        userBoneToHumanoidMappings.Clear();
-        
-        // instantiate a game object of the user avatar to save out bone parents then destroy it
-        UnityEngine.Object avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object));
-        GameObject assetGameObject = (GameObject)Instantiate(avatarResource);
-        SetParentNames(assetGameObject.transform, userParentNames);
-        DestroyImmediate(assetGameObject);
-        
-        // store joint mappings only for joints that exist in hifi and verify missing required joints
-        HumanBone[] boneMap = humanDescription.human;
-        string chestUserBone = "";
-        string neckUserBone = "";
-        foreach (HumanBone bone in boneMap) {
-            string humanName = bone.humanName;
-            string boneName = bone.boneName;
-            string hifiJointName;
-            if (HUMANOID_TO_HIFI_JOINT_NAME.TryGetValue(humanName, out hifiJointName)) {
-                userBoneToHumanoidMappings.Add(boneName, humanName);
-                if (humanName == "Chest") {
-                    chestUserBone = boneName;
-                } else if (humanName == "Neck") {
-                    neckUserBone = boneName;
-                }
-            }
-
-        }
-        if (!userBoneToHumanoidMappings.ContainsValue("Hips")) {
-            EditorUtility.DisplayDialog("Error", "There is no Hips bone in selected avatar", "Ok");
-            return false;
-        }
-        if (!userBoneToHumanoidMappings.ContainsValue("Spine")) {
-            EditorUtility.DisplayDialog("Error", "There is no Spine bone in selected avatar", "Ok");
-            return false;
-        }
-        if (!userBoneToHumanoidMappings.ContainsValue("Chest")) {
-            // check to see if there is a child of Spine that could be mapped to Chest
-            string spineChild = "";
-            foreach (var parentRelation in userParentNames) {
-                string humanName;
-                if (userBoneToHumanoidMappings.TryGetValue(parentRelation.Value, out humanName) && humanName == "Spine") {
-                    if (spineChild == "") {
-                        spineChild = parentRelation.Key;
-                    } else {
-                        // found more than one Spine child so we can't choose one to remap
-                        spineChild = "";
-                        break;
-                    }
-                }
-            }
-            if (spineChild != "" && !userBoneToHumanoidMappings.ContainsKey(spineChild)) {
-                // use child of Spine as Chest
-                userBoneToHumanoidMappings.Add(spineChild, "Chest");
-                chestUserBone = spineChild;
-            } else {
-                EditorUtility.DisplayDialog("Error", "There is no Chest bone in selected avatar", "Ok");
-                return false;
-            }
-        }
-        if (!userBoneToHumanoidMappings.ContainsValue("UpperChest")) {
-            //if parent of Neck is not Chest then map the parent to UpperChest
-            if (neckUserBone != "") {
-                string neckParentUserBone, neckParentHuman;
-                userParentNames.TryGetValue(neckUserBone, out neckParentUserBone);
-                userBoneToHumanoidMappings.TryGetValue(neckParentUserBone, out neckParentHuman);
-                if (neckParentHuman != "Chest" && !userBoneToHumanoidMappings.ContainsKey(neckParentUserBone)) {
-                    userBoneToHumanoidMappings.Add(neckParentUserBone, "UpperChest");
-                }
-            }
-            // if there is still no UpperChest bone but there is a Chest bone then we remap Chest to UpperChest 
-            if (!userBoneToHumanoidMappings.ContainsValue("UpperChest") && chestUserBone != "") {       
-                userBoneToHumanoidMappings[chestUserBone] = "UpperChest";
-            }
-        }
-        
-        return true;
-    }
-    
-    static void WriteFST(string exportFstPath, string projectName) {
-        userAbsoluteRotations.Clear();
-        
+    static void WriteFST(string exportFstPath, string projectName) {        
         // write out core fields to top of fst file
         try {
             File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " + 
@@ -403,49 +518,53 @@ class AvatarExporter : MonoBehaviour {
         }
         
         // write out joint mappings to fst file
-        foreach (var jointMapping in userBoneToHumanoidMappings) {
-            string hifiJointName = HUMANOID_TO_HIFI_JOINT_NAME[jointMapping.Value];
-            File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + jointMapping.Key + "\n");
+        foreach (var userBoneInfo in userBoneInfos) {
+            if (userBoneInfo.Value.HasHumanMapping()) {
+                string hifiJointName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneInfo.Value.humanName];
+                File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + userBoneInfo.Key + "\n");
+            }
         }
         
         // calculate and write out joint rotation offsets to fst file
         SkeletonBone[] skeletonMap = humanDescription.skeleton;
         foreach (SkeletonBone userBone in skeletonMap) {
             string userBoneName = userBone.name;
-            Quaternion userBoneRotation = userBone.rotation;            
-            
-            string parentName;
-            userParentNames.TryGetValue(userBoneName, out parentName);
-            if (parentName == "root") {
-                // if the parent is root then use bone's rotation
-                userAbsoluteRotations.Add(userBoneName, userBoneRotation);
-            } else {
-                // otherwise multiply bone's rotation by parent bone's absolute rotation
-                userAbsoluteRotations.Add(userBoneName, userAbsoluteRotations[parentName] * userBoneRotation);
+            UserBoneInformation userBoneInfo;
+            if (!userBoneInfos.TryGetValue(userBoneName, out userBoneInfo)) {
+                continue;
             }
             
-            // generate joint rotation offsets for both humanoid-mapped bones as well as extra unmapped bones in user avatar
+            Quaternion userBoneRotation = userBone.rotation;   
+            string parentName = userBoneInfo.parentName;
+            if (parentName == "root") {
+                // if the parent is root then use bone's rotation
+                userBoneInfo.rotation = userBoneRotation;
+            } else {
+                // otherwise multiply bone's rotation by parent bone's absolute rotation
+                userBoneInfo.rotation = userBoneInfos[parentName].rotation * userBoneRotation;
+            }
+            
+            // generate joint rotation offsets for both humanoid-mapped bones as well as extra unmapped bones
             Quaternion jointOffset = new Quaternion();
-            string humanName, outputJointName = "";
-            if (userBoneToHumanoidMappings.TryGetValue(userBoneName, out humanName)) {
-                outputJointName = HUMANOID_TO_HIFI_JOINT_NAME[humanName];
-                Quaternion rotation = referenceAbsoluteRotations[outputJointName];
-                jointOffset = Quaternion.Inverse(userAbsoluteRotations[userBoneName]) * rotation;
-            } else if (userAbsoluteRotations.ContainsKey(userBoneName)) {
+            string outputJointName = "";
+            if (userBoneInfo.HasHumanMapping()) {
+                outputJointName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneInfo.humanName];
+                Quaternion rotation = REFERENCE_ROTATIONS[userBoneInfo.humanName];
+                jointOffset = Quaternion.Inverse(userBoneInfo.rotation) * rotation;
+            } else {
                 outputJointName = userBoneName;
-                string lastRequiredParent = FindLastRequiredParentBone(userBoneName);
-                if (lastRequiredParent == "root") {
-                    jointOffset = Quaternion.Inverse(userAbsoluteRotations[userBoneName]);
-                } else {
+                jointOffset = Quaternion.Inverse(userBoneInfo.rotation);
+                string lastRequiredParent = FindLastRequiredAncestorBone(userBoneName);
+                if (lastRequiredParent != "root") {
                     // take the previous offset and multiply it by the current local when we have an extra joint
-                    string lastRequiredParentHifiName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneToHumanoidMappings[lastRequiredParent]];
-                    Quaternion lastRequiredParentRotation = referenceAbsoluteRotations[lastRequiredParentHifiName];
-                    jointOffset = Quaternion.Inverse(userAbsoluteRotations[userBoneName]) * lastRequiredParentRotation;
+                    string lastRequiredParentHumanName = userBoneInfos[lastRequiredParent].humanName;
+                    Quaternion lastRequiredParentRotation = REFERENCE_ROTATIONS[lastRequiredParentHumanName];
+                    jointOffset *= lastRequiredParentRotation;
                 }
             }
             
             // swap from left-handed (Unity) to right-handed (HiFi) coordinates and write out joint rotation offset to fst
-            if (outputJointName != "") {
+            if (!string.IsNullOrEmpty(outputJointName)) {
                 jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w);
                 File.AppendAllText(exportFstPath, "jointRotationOffset = " + outputJointName + " = (" + jointOffset.x + ", " +
                                                   jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n");
@@ -455,48 +574,326 @@ class AvatarExporter : MonoBehaviour {
         // open File Explorer to the project directory once finished
         System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath);
     }
-
-    static void SetParentNames(Transform modelBone, Dictionary<string, string> parentNames) {
-        for (int i = 0; i < modelBone.childCount; i++) {
-            SetParentNames(modelBone.GetChild(i), parentNames);
+    
+    static void SetUserBoneInformation() {
+        userBoneInfos.Clear();
+        humanoidToUserBoneMappings.Clear();
+        userBoneTree = new BoneTreeNode();
+        
+        // instantiate a game object of the user avatar to traverse the bone tree to gather  
+        // bone parents and positions as well as build a bone tree, then destroy it
+        UnityEngine.Object avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object));
+        GameObject assetGameObject = (GameObject)Instantiate(avatarResource);     
+        TraverseUserBoneTree(assetGameObject.transform);      
+        DestroyImmediate(assetGameObject);
+        
+        // iterate over Humanoid bones and update user bone info to increase human mapping counts for each bone
+        // as well as set their Humanoid name and build a Humanoid to user bone mapping
+        HumanBone[] boneMap = humanDescription.human;
+        foreach (HumanBone bone in boneMap) {
+            string humanName = bone.humanName;
+            string userBoneName = bone.boneName;
+            string hifiJointName;
+            if (userBoneInfos.ContainsKey(userBoneName)) {
+                ++userBoneInfos[userBoneName].mappingCount;
+                if (HUMANOID_TO_HIFI_JOINT_NAME.TryGetValue(humanName, out hifiJointName)) {
+                    userBoneInfos[userBoneName].humanName = humanName;
+                    humanoidToUserBoneMappings.Add(humanName, userBoneName);
+                }
+            }
         }
-        if (modelBone.parent != null) {
-            parentNames.Add(modelBone.name, modelBone.parent.name);
-        } else {
-            parentNames.Add(modelBone.name, "root");
+        
+        // generate the list of bone rule failure strings for any bone rules that are not satisfied by this avatar
+        SetFailedBoneRules();
+    }
+
+    static void TraverseUserBoneTree(Transform modelBone) {
+        GameObject gameObject = modelBone.gameObject;
+
+        // check if this transform is a node containing mesh, light, or camera instead of a bone
+        bool mesh = gameObject.GetComponent<MeshRenderer>() != null || gameObject.GetComponent<SkinnedMeshRenderer>() != null;
+        bool light = gameObject.GetComponent<Light>() != null;
+        bool camera = gameObject.GetComponent<Camera>() != null;
+        
+        // if it is in fact a bone, add it to the bone tree as well as user bone infos list with position and parent name
+        if (!mesh && !light && !camera) {
+            UserBoneInformation userBoneInfo = new UserBoneInformation();
+            userBoneInfo.position = modelBone.position; // bone's absolute position
+            
+            string boneName = modelBone.name;
+            if (modelBone.parent == null) {
+                // if no parent then this is actual root bone node of the user avatar, so consider it's parent as "root"
+                userBoneTree = new BoneTreeNode(boneName); // initialize root of tree
+                userBoneInfo.parentName = "root";
+                userBoneInfo.boneTreeNode = userBoneTree;
+            } else {
+                // otherwise add this bone node as a child to it's parent's children list
+                string parentName = modelBone.parent.name;
+                BoneTreeNode boneTreeNode = new BoneTreeNode(boneName);
+                userBoneInfos[parentName].boneTreeNode.children.Add(boneTreeNode);
+                userBoneInfo.parentName = parentName;
+            }
+
+            userBoneInfos.Add(boneName, userBoneInfo);
+        }
+        
+        // recurse over transform node's children
+        for (int i = 0; i < modelBone.childCount; ++i) {
+            TraverseUserBoneTree(modelBone.GetChild(i));
         }
     }
     
-    static string FindLastRequiredParentBone(string currentBone) {
+    static string FindLastRequiredAncestorBone(string currentBone) {
         string result = currentBone;
-        while (result != "root" && !userBoneToHumanoidMappings.ContainsKey(result)) {
-            result = userParentNames[result];            
+        // iterating upward through user bone info parent names, find the first ancestor bone that is mapped in Humanoid
+        while (result != "root" && userBoneInfos.ContainsKey(result) && !userBoneInfos[result].HasHumanMapping()) {
+            result = userBoneInfos[result].parentName;            
         }
         return result;
     }
+    
+    static void SetFailedBoneRules() {
+        failedBoneRules.Clear();
+        
+        string hipsUserBone = "";
+        string spineUserBone = "";
+        string chestUserBone = "";
+        string headUserBone = "";
+        
+        Vector3 hipsPosition = new Vector3();
+        
+        // iterate over all bone rules in order and add any rules that fail 
+        // to the failed bone rules map with appropriate error or warning text
+        for (BoneRule boneRule = 0; boneRule < BoneRule.BoneRuleEnd; ++boneRule) { 
+            switch (boneRule) {
+                case BoneRule.RecommendedUnityVersion:
+                    if (Array.IndexOf(RECOMMENDED_UNITY_VERSIONS, Application.unityVersion) == -1) {
+                        failedBoneRules.Add(boneRule, "The current version of Unity is not one of the recommended Unity " +
+                                                      "versions. If you are using a version of Unity later than 2018.2.12f1, " +
+                                                      "it is recommended to apply Enforce T-Pose under the Pose dropdown " +
+                                                      "in Humanoid configuration.");
+                    }
+                    break;
+                case BoneRule.SingleRoot:
+                    // bone rule fails if the root bone node has more than one child bone
+                    if (userBoneTree.children.Count > 1) {
+                        failedBoneRules.Add(boneRule, "There is more than one bone at the top level of the selected avatar's " +
+                                                      "bone hierarchy. Please ensure all bones for Humanoid mappings are " +
+                                                      "under the same bone hierarchy.");
+                    }
+                    break;
+                case BoneRule.NoDuplicateMapping:
+                    // bone rule fails if any user bone is mapped to more than one Humanoid bone
+                    foreach (var userBoneInfo in userBoneInfos) {
+                        string boneName = userBoneInfo.Key;
+                        int mappingCount = userBoneInfo.Value.mappingCount;
+                        if (mappingCount > 1) {
+                            string text = "The " + boneName + " bone is mapped to more than one bone in Humanoid.";
+                            if (failedBoneRules.ContainsKey(boneRule)) {
+                                failedBoneRules[boneRule] += "\n" + text;
+                            } else {
+                                failedBoneRules.Add(boneRule, text);
+                            }
+                        }
+                    }
+                    break;
+                case BoneRule.NoAsymmetricalLegMapping:
+                    CheckAsymmetricalMappingRule(boneRule, LEG_MAPPING_SUFFIXES, "leg");
+                    break;
+                case BoneRule.NoAsymmetricalArmMapping:
+                    CheckAsymmetricalMappingRule(boneRule, ARM_MAPPING_SUFFIXES, "arm");
+                    break;
+                case BoneRule.NoAsymmetricalHandMapping:
+                    CheckAsymmetricalMappingRule(boneRule, HAND_MAPPING_SUFFIXES, "hand");
+                    break;
+                case BoneRule.HipsMapped:
+                    hipsUserBone = CheckHumanBoneMappingRule(boneRule, "Hips");
+                    break;
+                case BoneRule.SpineMapped:
+                    spineUserBone = CheckHumanBoneMappingRule(boneRule, "Spine");
+                    break;
+                case BoneRule.SpineDescendantOfHips:
+                    CheckUserBoneDescendantOfHumanRule(boneRule, spineUserBone, "Hips");
+                    break;
+                case BoneRule.ChestMapped:
+                    if (!humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) {
+                        // check to see if there is a child of Spine that we can suggest to be mapped to Chest
+                        string spineChild = "";
+                        if (!string.IsNullOrEmpty(spineUserBone)) {
+                            BoneTreeNode spineTreeNode = userBoneInfos[spineUserBone].boneTreeNode;
+                            if (spineTreeNode.children.Count == 1) {
+                                spineChild = spineTreeNode.children[0].boneName;
+                            }
+                        }
+                        failedBoneRules.Add(boneRule, "There is no Chest bone mapped in Humanoid for the selected avatar.");
+                        // if the only found child of Spine is not yet mapped then add it as a suggestion for Chest mapping 
+                        if (!string.IsNullOrEmpty(spineChild) && !userBoneInfos[spineChild].HasHumanMapping()) {
+                            failedBoneRules[boneRule] += " It is suggested that you map bone " + spineChild + 
+                                                         " to Chest in Humanoid.";
+                        }
+                    }
+                    break;
+                case BoneRule.ChestDescendantOfSpine:
+                    CheckUserBoneDescendantOfHumanRule(boneRule, chestUserBone, "Spine");
+                    break;
+                case BoneRule.NeckMapped:
+                    CheckHumanBoneMappingRule(boneRule, "Neck");
+                    break;
+                case BoneRule.HeadMapped:
+                    headUserBone = CheckHumanBoneMappingRule(boneRule, "Head");
+                    break;
+                case BoneRule.HeadDescendantOfChest:
+                    CheckUserBoneDescendantOfHumanRule(boneRule, headUserBone, "Chest");
+                    break;
+                case BoneRule.EyesMapped:
+                    bool leftEyeMapped = humanoidToUserBoneMappings.ContainsKey("LeftEye");
+                    bool rightEyeMapped = humanoidToUserBoneMappings.ContainsKey("RightEye");
+                    if (!leftEyeMapped || !rightEyeMapped) {
+                        if (leftEyeMapped && !rightEyeMapped) {
+                            failedBoneRules.Add(boneRule, "There is no RightEye bone mapped in Humanoid " + 
+                                                          "for the selected avatar.");
+                        } else if (!leftEyeMapped && rightEyeMapped) {
+                            failedBoneRules.Add(boneRule, "There is no LeftEye bone mapped in Humanoid " + 
+                                                          "for the selected avatar.");
+                        } else {
+                            failedBoneRules.Add(boneRule, "There is no LeftEye or RightEye bone mapped in Humanoid " + 
+                                                          "for the selected avatar.");
+                        }
+                    }
+                    break;
+                case BoneRule.HipsNotOnGround:
+                    // ensure the absolute Y position for the bone mapped to Hips (if its mapped) is at least HIPS_GROUND_MIN_Y
+                    if (!string.IsNullOrEmpty(hipsUserBone)) {
+                        UserBoneInformation hipsBoneInfo = userBoneInfos[hipsUserBone];
+                        hipsPosition = hipsBoneInfo.position;
+                        if (hipsPosition.y < HIPS_GROUND_MIN_Y) {
+                            failedBoneRules.Add(boneRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + 
+                                                          ") should not be at ground level.");
+                        }
+                    }
+                    break;
+                case BoneRule.HipsSpineChestNotCoincident:
+                    // ensure the bones mapped to Hips, Spine, and Chest are all not in the same position,
+                    // check Hips to Spine and Spine to Chest lengths are within HIPS_SPINE_CHEST_MIN_SEPARATION
+                    if (!string.IsNullOrEmpty(spineUserBone) && !string.IsNullOrEmpty(chestUserBone) && 
+                        !string.IsNullOrEmpty(hipsUserBone)) {
+                        UserBoneInformation spineBoneInfo = userBoneInfos[spineUserBone];
+                        UserBoneInformation chestBoneInfo = userBoneInfos[chestUserBone];
+                        Vector3 hipsToSpine = hipsPosition - spineBoneInfo.position;
+                        Vector3 spineToChest = spineBoneInfo.position - chestBoneInfo.position;
+                        if (hipsToSpine.magnitude < HIPS_SPINE_CHEST_MIN_SEPARATION && 
+                            spineToChest.magnitude < HIPS_SPINE_CHEST_MIN_SEPARATION) {
+                            failedBoneRules.Add(boneRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + 
+                                                          "), the bone mapped to Spine in Humanoid (" + spineUserBone + 
+                                                          "), and the bone mapped to Chest in Humanoid (" + chestUserBone + 
+                                                          ") should not be coincidental.");
+                        }
+                    }
+                    break;
+                case BoneRule.TotalBoneCountUnderLimit:
+                    int userBoneCount = userBoneInfos.Count;
+                    if (userBoneCount > MAXIMUM_USER_BONE_COUNT) {
+                        failedBoneRules.Add(boneRule, "The total number of bones in the avatar (" + userBoneCount + 
+                                                      ") exceeds the maximum bone limit (" + MAXIMUM_USER_BONE_COUNT + ").");
+                    }
+                    break;
+            }
+        }
+    }
+    
+    static string CheckHumanBoneMappingRule(BoneRule boneRule, string humanBoneName) {
+        string userBoneName = "";
+        // bone rule fails if bone is not mapped in Humanoid
+        if (!humanoidToUserBoneMappings.TryGetValue(humanBoneName, out userBoneName)) {
+            failedBoneRules.Add(boneRule, "There is no " + humanBoneName + " bone mapped in Humanoid for the selected avatar.");
+        }
+        return userBoneName;
+    }
+    
+    static void CheckUserBoneDescendantOfHumanRule(BoneRule boneRule, string userBoneName, string descendantOfHumanName) {
+        if (string.IsNullOrEmpty(userBoneName)) {
+            return;
+        }
+        
+        string descendantOfUserBoneName = "";
+        if (!humanoidToUserBoneMappings.TryGetValue(descendantOfHumanName, out descendantOfUserBoneName)) {
+            return;
+        }
+
+        string userBone = userBoneName;
+        string ancestorUserBone = "";
+        UserBoneInformation userBoneInfo = new UserBoneInformation();
+        // iterate upward from user bone through user bone info parent names until root 
+        // is reached or the ancestor bone name matches the target descendant of name
+        while (ancestorUserBone != "root") {
+            if (userBoneInfos.TryGetValue(userBone, out userBoneInfo)) {
+                ancestorUserBone = userBoneInfo.parentName;
+                if (ancestorUserBone == descendantOfUserBoneName) {
+                    return;
+                }
+                userBone = ancestorUserBone;
+            } else {
+                break;
+            }
+        }
+        
+        // bone rule fails if no ancestor of given user bone matched the descendant of name (no early return)
+        failedBoneRules.Add(boneRule, "The bone mapped to " + userBoneInfo.humanName + " in Humanoid (" + userBoneName + 
+                                      ") is not a child of the bone mapped to " + descendantOfHumanName + " in Humanoid (" + 
+                                      descendantOfUserBoneName + ").");
+    }
+    
+    static void CheckAsymmetricalMappingRule(BoneRule boneRule, string[] mappingSuffixes, string appendage) {
+        int leftCount = 0;
+        int rightCount = 0;
+        // add Left/Right to each mapping suffix to make Humanoid mapping names, 
+        // and count the number of bones mapped in Humanoid on each side
+        foreach (string mappingSuffix in mappingSuffixes) {
+            string leftMapping = "Left" + mappingSuffix;
+            string rightMapping = "Right" + mappingSuffix;
+            if (humanoidToUserBoneMappings.ContainsKey(leftMapping)) {
+                ++leftCount;
+            }
+            if (humanoidToUserBoneMappings.ContainsKey(rightMapping)) {
+                ++rightCount;
+            }
+        }
+        // bone rule fails if number of left appendage mappings doesn't match number of right appendage mappings 
+        if (leftCount != rightCount) {
+            failedBoneRules.Add(boneRule, "The number of bones mapped in Humanoid for the left " + appendage + " (" +
+                                          leftCount + ") does not match the number of bones mapped in Humanoid for the right " +
+                                          appendage + " (" + rightCount + ").");
+        }
+    }
 }
 
 class ExportProjectWindow : EditorWindow {
-    const int MIN_WIDTH = 450;
-    const int MIN_HEIGHT = 250;
+    const int WINDOW_WIDTH = 500;
+    const int WINDOW_HEIGHT = 460;
     const int BUTTON_FONT_SIZE = 16;
     const int LABEL_FONT_SIZE = 16;
     const int TEXT_FIELD_FONT_SIZE = 14;
     const int TEXT_FIELD_HEIGHT = 20;
     const int ERROR_FONT_SIZE = 12;
+    const int WARNING_SCROLL_HEIGHT = 170;
+    const string EMPTY_ERROR_TEXT = "None\n";
     
     string projectName = "";
     string projectLocation = "";
     string projectDirectory = "";
-    string errorLabel = "\n";
+    string errorText = EMPTY_ERROR_TEXT;
+    string warningText = "";
+    Vector2 warningScrollPosition = new Vector2(0, 0);
     
-    public delegate void OnCloseDelegate(string projectDirectory, string projectName);
+    public delegate void OnCloseDelegate(string projectDirectory, string projectName, string warnings);
     OnCloseDelegate onCloseCallback;
     
-    public void Init(string initialPath, OnCloseDelegate closeCallback) {
-        minSize = new Vector2(MIN_WIDTH, MIN_HEIGHT);
+    public void Init(string initialPath, string warnings, OnCloseDelegate closeCallback) {
+        minSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT);
+        maxSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT);
         titleContent.text = "Export New Avatar";
         projectLocation = initialPath;
+        warningText = warnings;
         onCloseCallback = closeCallback;
         ShowUtility();
     }
@@ -513,6 +910,9 @@ class ExportProjectWindow : EditorWindow {
         GUIStyle errorStyle = new GUIStyle(GUI.skin.label); 
         errorStyle.fontSize = ERROR_FONT_SIZE;
         errorStyle.normal.textColor = Color.red;
+        errorStyle.wordWrap = true;
+        GUIStyle warningStyle = new GUIStyle(errorStyle); 
+        warningStyle.normal.textColor = Color.yellow;
      
         GUILayout.Space(10);
         
@@ -534,10 +934,20 @@ class ExportProjectWindow : EditorWindow {
             }
         }
         
-        // Red error label text to display any issues under text fields and Browse button
-        GUILayout.Label(errorLabel, errorStyle);
+        // Red error label text to display any file-related errors
+        GUILayout.Label("Error:", errorStyle);
+        GUILayout.Label(errorText, errorStyle);
         
-        GUILayout.Space(20);
+        GUILayout.Space(10);
+        
+        // Yellow warning label text to display scrollable list of any bone-related warnings 
+        GUILayout.Label("Warnings:", warningStyle);
+        warningScrollPosition = GUILayout.BeginScrollView(warningScrollPosition, GUILayout.Width(WINDOW_WIDTH), 
+                                                          GUILayout.Height(WARNING_SCROLL_HEIGHT));
+        GUILayout.Label(warningText, warningStyle);
+        GUILayout.EndScrollView();
+        
+        GUILayout.Space(10);
         
         // Export button which will verify project folder can actually be created 
         // before closing popup window and calling back to initiate the export
@@ -546,7 +956,7 @@ class ExportProjectWindow : EditorWindow {
             export = true;
             if (!CheckForErrors(true)) {
                 Close();
-                onCloseCallback(projectDirectory, projectName);
+                onCloseCallback(projectDirectory, projectName, warningText);
             }
         }
         
@@ -562,12 +972,12 @@ class ExportProjectWindow : EditorWindow {
     }
     
     bool CheckForErrors(bool exporting) {   
-        errorLabel = "\n"; // default to no error
+        errorText = EMPTY_ERROR_TEXT; // default to None if no errors found
         projectDirectory = projectLocation + "\\" + projectName + "\\";
         if (projectName.Length > 0) {
             // new project must have a unique folder name since the folder will be created for it
             if (Directory.Exists(projectDirectory)) {
-                errorLabel = "A folder with the name " + projectName + 
+                errorText = "A folder with the name " + projectName + 
                              " already exists at that location.\nPlease choose a different project name or location.";
                 return true;
             }
@@ -575,7 +985,7 @@ class ExportProjectWindow : EditorWindow {
         if (projectLocation.Length > 0) {
             // before clicking Export we can verify that the project location at least starts with a drive
             if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') {
-                errorLabel = "Project location is invalid. Please choose a different project location.\n";
+                errorText = "Project location is invalid. Please choose a different project location.\n";
                 return true;
             }
         }
@@ -583,16 +993,16 @@ class ExportProjectWindow : EditorWindow {
             // when exporting, project name and location must both be defined, and project location must
             // be valid and accessible (we attempt to create the project folder at this time to verify this)
             if (projectName.Length == 0) {
-                errorLabel = "Please define a project name.\n";
+                errorText = "Please define a project name.\n";
                 return true;
             } else if (projectLocation.Length == 0) {
-                errorLabel = "Please define a project location.\n";
+                errorText = "Please define a project location.\n";
                 return true;
             } else {
                 try {
                     Directory.CreateDirectory(projectDirectory);
                 } catch {
-                    errorLabel = "Project location is invalid. Please choose a different project location.\n";
+                    errorText = "Project location is invalid. Please choose a different project location.\n";
                     return true;
                 }
             }
diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt
index 3ca4dbb1ee..f02bc688ae 100644
--- a/tools/unity-avatar-exporter/Assets/README.txt
+++ b/tools/unity-avatar-exporter/Assets/README.txt
@@ -1,3 +1,7 @@
+High Fidelity, Inc.
+Avatar Exporter
+Version 0.1
+
 Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter.
 
 To create a new avatar project:
diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage
index 28052efea5..5e825bd0d9 100644
Binary files a/tools/unity-avatar-exporter/avatarExporter.unitypackage and b/tools/unity-avatar-exporter/avatarExporter.unitypackage differ