diff --git a/Editor/HeuristicBoneMapper.cs b/Editor/HeuristicBoneMapper.cs index b1ba169b..9c4b03ee 100644 --- a/Editor/HeuristicBoneMapper.cs +++ b/Editor/HeuristicBoneMapper.cs @@ -243,6 +243,12 @@ namespace nadena.dev.modular_avatar.core.editor internal static readonly ImmutableDictionary> NameToBoneMap; internal static readonly ImmutableDictionary> BoneToNameMap; + [InitializeOnLoadMethod] + private static void InsertboneNamePatternsToRuntime() + { + ModularAvatarMergeArmature.boneNamePatterns = boneNamePatterns; + } + static HeuristicBoneMapper() { var pat_end_side = new Regex(@"[_\.]([LR])$"); @@ -306,7 +312,9 @@ namespace nadena.dev.modular_avatar.core.editor GameObject src, GameObject newParent, List skipped = null, - HashSet unassigned = null + HashSet unassigned = null, + Animator avatarAnimator = null, + Dictionary outfitHumanoidBones = null ) { Dictionary mappings = new Dictionary(); @@ -355,21 +363,65 @@ namespace nadena.dev.modular_avatar.core.editor var childName = child.gameObject.name; var targetObjectName = childName.Substring(config.prefix.Length, childName.Length - config.prefix.Length - config.suffix.Length); - - if (!NameToBoneMap.TryGetValue( - NormalizeName(targetObjectName), out var bodyBones)) + List bodyBones = null; + var isMapped = false; + + if (outfitHumanoidBones != null && outfitHumanoidBones.TryGetValue(child, out var outfitHumanoidBone)) + { + if (avatarAnimator != null) + { + var avatarBone = avatarAnimator.GetBoneTransform(outfitHumanoidBone); + if (avatarBone != null && unassigned.Contains(avatarBone)) + { + mappings[child] = avatarBone; + unassigned.Remove(avatarBone); + lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name)); + isMapped = true; + } else { + bodyBones = new List { outfitHumanoidBone }; + } + } else { + bodyBones = new List() { outfitHumanoidBone }; + } + } + + if (!isMapped && bodyBones == null && !NameToBoneMap.TryGetValue( + NormalizeName(targetObjectName), out bodyBones)) { continue; } - foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone])) + if (!isMapped) { - if (lcNameToXform.TryGetValue(otherName, out var targetObject)) + foreach (var bodyBone in bodyBones) { - mappings[child] = targetObject; - unassigned.Remove(targetObject); - lcNameToXform.Remove(otherName.ToLowerInvariant()); - break; + if (avatarAnimator != null) + { + var avatarBone = avatarAnimator.GetBoneTransform(bodyBone); + if (avatarBone != null && unassigned.Contains(avatarBone)) + { + mappings[child] = avatarBone; + unassigned.Remove(avatarBone); + lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name)); + isMapped = true; + break; + } + } + } + } + + if (!isMapped) + { + foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone])) + { + if (lcNameToXform.TryGetValue(otherName, out var targetObject)) + { + mappings[child] = targetObject; + unassigned.Remove(targetObject); + lcNameToXform.Remove(otherName.ToLowerInvariant()); + isMapped = true; + break; + } } } @@ -388,7 +440,7 @@ namespace nadena.dev.modular_avatar.core.editor return mappings; } - internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List skipped = null) + internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List skipped = null, Dictionary outfitHumanoidBones = null, Animator avatarAnimator = null) { var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform)); if (target == null) return; @@ -399,7 +451,7 @@ namespace nadena.dev.modular_avatar.core.editor void Traverse(Transform src, Transform dst) { - var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped); + var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator); foreach (var pair in mappings) { diff --git a/Editor/Inspector/MergeArmatureEditor.cs b/Editor/Inspector/MergeArmatureEditor.cs index 0fd215b4..0dbf58f0 100644 --- a/Editor/Inspector/MergeArmatureEditor.cs +++ b/Editor/Inspector/MergeArmatureEditor.cs @@ -100,7 +100,7 @@ namespace nadena.dev.modular_avatar.core.editor { serializedObject.ApplyModifiedProperties(); - if (target.mergeTargetObject != null && priorMergeTarget == null + if (target.mergeTargetObject != null && priorMergeTarget != target.mergeTargetObject && string.IsNullOrEmpty(target.prefix) && string.IsNullOrEmpty(target.suffix)) { @@ -115,7 +115,27 @@ namespace nadena.dev.modular_avatar.core.editor { if (GUILayout.Button(G("merge_armature.adjust_names"))) { - HeuristicBoneMapper.RenameBonesByHeuristic(target); + var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(target.mergeTarget.Get(target).transform); + var avatarAnimator = avatarRoot != null ? avatarRoot.GetComponent() : null; + + // Search Outfit Root Animator + var outfitRoot = ((ModularAvatarMergeArmature)serializedObject.targetObject).transform; + Animator outfitAnimator = null; + while (outfitRoot != null) + { + if (outfitRoot == avatarRoot) + { + outfitAnimator = null; + break; + } + outfitAnimator = outfitRoot.GetComponent(); + if (outfitAnimator != null && outfitAnimator.isHuman) break; + outfitAnimator = null; + outfitRoot = outfitRoot.parent; + } + + var outfitHumanoidBones = SetupOutfit.GetOutfitHumanoidBones(outfitRoot, outfitAnimator); + HeuristicBoneMapper.RenameBonesByHeuristic(target, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator); } } diff --git a/Editor/SetupOutfit.cs b/Editor/SetupOutfit.cs index c09578c8..f82510a3 100644 --- a/Editor/SetupOutfit.cs +++ b/Editor/SetupOutfit.cs @@ -8,6 +8,7 @@ using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; using static nadena.dev.modular_avatar.core.editor.Localization; +using System; #endregion @@ -172,8 +173,11 @@ namespace nadena.dev.modular_avatar.core.editor PrefabUtility.RecordPrefabInstancePropertyModifications(merge); + var outfitAnimator = outfitRoot.GetComponent(); + var outfitHumanoidBones = GetOutfitHumanoidBones(outfitRoot.transform, outfitAnimator); + var avatarAnimator = avatarRoot.GetComponent(); List subRoots = new List(); - HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots); + HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator); // If the outfit has an UpperChest bone but the avatar doesn't, add an additional MergeArmature to // help with this @@ -218,7 +222,6 @@ namespace nadena.dev.modular_avatar.core.editor outfitArmature.name += ".1"; // Also make sure to refresh the avatar's animator humanoid bone cache. - var avatarAnimator = avatarRoot.GetComponent(); var humanDescription = avatarAnimator.avatar; avatarAnimator.avatar = null; // ReSharper disable once Unity.InefficientPropertyAccess @@ -274,6 +277,37 @@ namespace nadena.dev.modular_avatar.core.editor } } + internal static Dictionary GetOutfitHumanoidBones(Transform outfitRoot, Animator outfitAnimator) + { + if (outfitAnimator != null) + { + var hipsCheck = outfitAnimator.isHuman ? outfitAnimator.GetBoneTransform(HumanBodyBones.Hips) : null; + if (hipsCheck != null && hipsCheck.parent == outfitRoot) + { + // Sometimes broken rigs can have the hips as a direct child of the root, instead of having + // an intermediate Armature object. We do not currently support this kind of rig, and so we'll + // assume the outfit's humanoid rig is broken and move on to heuristic matching. + outfitAnimator = null; + } else if (hipsCheck == null) { + outfitAnimator = null; + } + } + + Dictionary outfitHumanoidBones = null; + if (outfitAnimator != null) + { + outfitHumanoidBones = new Dictionary(); + foreach (HumanBodyBones boneIndex in Enum.GetValues(typeof(HumanBodyBones))) + { + var bone = boneIndex != HumanBodyBones.LastBone ? outfitAnimator.GetBoneTransform(boneIndex) : null; + if (bone == null) continue; + outfitHumanoidBones[bone] = boneIndex; + } + } + + return outfitHumanoidBones; + } + internal static void FixAPose(GameObject avatarRoot, Transform outfitArmature, bool strictMode = true) { var mergeArmature = outfitArmature.GetComponent(); @@ -540,6 +574,7 @@ namespace nadena.dev.modular_avatar.core.editor } var hipsCandidates = new List(); + var hipsExtraCandidateRoots = new List(); if (outfitHips == null) { @@ -548,6 +583,23 @@ namespace nadena.dev.modular_avatar.core.editor foreach (Transform child in outfitRoot.transform) { foreach (Transform tempHip in child) + { + if (tempHip.name.Contains(avatarHips.name)) + { + outfitHips = tempHip.gameObject; + // Prefer the first hips we find + break; + } + hipsExtraCandidateRoots.Add(tempHip); + } + + if (outfitHips != null) return true; // found an exact match, bail outgit + } + + // Sometimes, Hips is in deeper place(like root -> Armature -> Armature 1 -> Hips). + foreach (Transform extraCandidateRoot in hipsExtraCandidateRoots) + { + foreach (Transform tempHip in extraCandidateRoot) { if (tempHip.name.Contains(avatarHips.name)) { @@ -561,6 +613,7 @@ namespace nadena.dev.modular_avatar.core.editor } hipsCandidates.Add(avatarHips.name); + hipsExtraCandidateRoots = new List(); // If that doesn't work out, we'll check for heuristic bone mapper mappings. foreach (var hbm in HeuristicBoneMapper.BoneToNameMap[HumanBodyBones.Hips]) @@ -581,6 +634,25 @@ namespace nadena.dev.modular_avatar.core.editor { outfitHips = tempHip.gameObject; } + hipsExtraCandidateRoots.Add(tempHip); + } + } + } + + if (outfitHips == null) + { + // Sometimes, Hips is in deeper place(like root -> Armature -> Armature 1 -> Hips). + foreach (Transform extraCandidateRoot in hipsExtraCandidateRoots) + { + foreach (Transform tempHip in extraCandidateRoot) + { + foreach (var candidate in hipsCandidates) + { + if (HeuristicBoneMapper.NormalizeName(tempHip.name).Contains(candidate)) + { + outfitHips = tempHip.gameObject; + } + } } } } diff --git a/Runtime/ModularAvatarMergeArmature.cs b/Runtime/ModularAvatarMergeArmature.cs index 36e5e36e..7f7c3225 100644 --- a/Runtime/ModularAvatarMergeArmature.cs +++ b/Runtime/ModularAvatarMergeArmature.cs @@ -61,6 +61,9 @@ namespace nadena.dev.modular_avatar.core public bool mangleNames = true; + // Inserted from HeuristicBoneMapper(Editor Assembly) with InitializeOnLoadMethod + // We use raw `boneNamePatterns` instead of `BoneToNameMap` because BoneToNameMap requires matching with normalized bone name, but normalizing makes raw prefix/suffix unavailable. + internal static string[][] boneNamePatterns; private ArmatureLockController _lockController; internal Transform MapBone(Transform bone) @@ -216,14 +219,30 @@ namespace nadena.dev.modular_avatar.core // GameObject we're attached to. var baseName = hips.name; var mergeName = transform.GetChild(0).name; + var isInferred = false; - var prefixLength = mergeName.IndexOf(baseName, StringComparison.InvariantCulture); - if (prefixLength < 0) return; + foreach (var hipNameCandidate in boneNamePatterns[(int)HumanBodyBones.Hips]) + { + var prefixLength = mergeName.IndexOf(hipNameCandidate, StringComparison.InvariantCultureIgnoreCase); + if (prefixLength < 0) continue; - var suffixLength = mergeName.Length - prefixLength - baseName.Length; + var suffixLength = mergeName.Length - prefixLength - hipNameCandidate.Length; - prefix = mergeName.Substring(0, prefixLength); - suffix = mergeName.Substring(mergeName.Length - suffixLength); + prefix = mergeName.Substring(0, prefixLength); + suffix = mergeName.Substring(mergeName.Length - suffixLength); + isInferred = true; + break; + } + + if (!isInferred) { // Also check with old method as fallback + var prefixLength = mergeName.IndexOf(baseName, StringComparison.InvariantCulture); + if (prefixLength < 0) return; + + var suffixLength = mergeName.Length - prefixLength - baseName.Length; + + prefix = mergeName.Substring(0, prefixLength); + suffix = mergeName.Substring(mergeName.Length - suffixLength); + } if (prefix == "J_Bip_C_") { @@ -242,4 +261,4 @@ namespace nadena.dev.modular_avatar.core if (mergeTarget != null) yield return mergeTarget; } } -} \ No newline at end of file +} diff --git a/UnitTests~/EasySetupOutfit/PreferFirstHipsMatch.cs b/UnitTests~/EasySetupOutfit/HipsMatchTest.cs similarity index 54% rename from UnitTests~/EasySetupOutfit/PreferFirstHipsMatch.cs rename to UnitTests~/EasySetupOutfit/HipsMatchTest.cs index 717e5332..46195fcf 100644 --- a/UnitTests~/EasySetupOutfit/PreferFirstHipsMatch.cs +++ b/UnitTests~/EasySetupOutfit/HipsMatchTest.cs @@ -4,7 +4,7 @@ using nadena.dev.modular_avatar.core.editor; using NUnit.Framework; using UnityEngine; -public class PreferFirstHipsMatch : TestBase +public class HipsMatchTest : TestBase { [Test] public void SetupHeuristicPrefersFirstHipsMatch() @@ -21,7 +21,28 @@ public class PreferFirstHipsMatch : TestBase var outfit = CreateChild(root, "Outfit"); var outfit_armature = CreateChild(outfit, "Armature"); var outfit_hips = CreateChild(outfit_armature, "Hips"); - + + Assert.IsTrue(SetupOutfit.FindBones(outfit, out var det_av_root, out var det_av_hips, out var det_outfit_hips)); + Assert.AreSame(root, det_av_root); + Assert.AreSame(root_hips, det_av_hips); + Assert.AreSame(outfit_hips, det_outfit_hips); + } + + [Test] + public void TestOutfitDeepHipsMatch() + { + var root = CreateCommonPrefab("shapell.fbx"); +#if MA_VRCSDK3_AVATARS + root.AddComponent(); +#endif + var root_hips = root.GetComponent().GetBoneTransform(HumanBodyBones.Hips).gameObject; + root_hips.name = "hip"; + + var outfit = CreateChild(root, "Outfit"); + var outfit_armature = CreateChild(outfit, "armature"); + var outfit_armature2 = CreateChild(outfit_armature, "armature2"); + var outfit_hips = CreateChild(outfit_armature2, "hips"); + Assert.IsTrue(SetupOutfit.FindBones(outfit, out var det_av_root, out var det_av_hips, out var det_outfit_hips)); Assert.AreSame(root, det_av_root); Assert.AreSame(root_hips, det_av_hips); diff --git a/UnitTests~/EasySetupOutfit/PreferFirstHipsMatch.cs.meta b/UnitTests~/EasySetupOutfit/HipsMatchTest.cs.meta similarity index 100% rename from UnitTests~/EasySetupOutfit/PreferFirstHipsMatch.cs.meta rename to UnitTests~/EasySetupOutfit/HipsMatchTest.cs.meta diff --git a/UnitTests~/EasySetupOutfit/InferPrefixSuffixTest.cs b/UnitTests~/EasySetupOutfit/InferPrefixSuffixTest.cs new file mode 100644 index 00000000..24c96a52 --- /dev/null +++ b/UnitTests~/EasySetupOutfit/InferPrefixSuffixTest.cs @@ -0,0 +1,108 @@ +using modular_avatar_tests; +using nadena.dev.modular_avatar.core; +using NUnit.Framework; +using UnityEngine; + +public class InferPrefixSuffixTest : TestBase +{ + [Test] + public void TestNoPrefixSuffix() + { + var root = CreateCommonPrefab("shapell.fbx"); +#if MA_VRCSDK3_AVATARS + root.AddComponent(); +#endif + var root_hips = root.GetComponent().GetBoneTransform(HumanBodyBones.Hips); + root_hips.name = "hip"; + + var outfit = CreateChild(root, "Outfit"); + var outfit_armature = CreateChild(outfit, "armature"); + var outfit_hips = CreateChild(outfit_armature, "hips"); + + var outfit_mama = outfit_armature.AddComponent(); + outfit_mama.mergeTarget = new AvatarObjectReference(); + outfit_mama.mergeTarget.referencePath = RuntimeUtil.RelativePath(root, root_hips.parent.gameObject); + outfit_mama.LockMode = ArmatureLockMode.BaseToMerge; + + outfit_mama.InferPrefixSuffix(); + + Assert.AreEqual("", outfit_mama.prefix); + Assert.AreEqual("", outfit_mama.suffix); + } + + [Test] + public void TestDifferentHipsName() + { + var root = CreateCommonPrefab("shapell.fbx"); +#if MA_VRCSDK3_AVATARS + root.AddComponent(); +#endif + var root_hips = root.GetComponent().GetBoneTransform(HumanBodyBones.Hips); + root_hips.name = "hip"; + + var outfit = CreateChild(root, "Outfit"); + var outfit_armature = CreateChild(outfit, "armature"); + var outfit_hips = CreateChild(outfit_armature, "pre_Hips.suf"); + + var outfit_mama = outfit_armature.AddComponent(); + outfit_mama.mergeTarget = new AvatarObjectReference(); + outfit_mama.mergeTarget.referencePath = RuntimeUtil.RelativePath(root, root_hips.parent.gameObject); + outfit_mama.LockMode = ArmatureLockMode.BaseToMerge; + + outfit_mama.InferPrefixSuffix(); + + Assert.AreEqual("pre_", outfit_mama.prefix); + Assert.AreEqual(".suf", outfit_mama.suffix); + } + + [Test] + public void TestSameHipsName_Success() + { + var root = CreateCommonPrefab("shapell.fbx"); +#if MA_VRCSDK3_AVATARS + root.AddComponent(); +#endif + var root_hips = root.GetComponent().GetBoneTransform(HumanBodyBones.Hips); + root_hips.name = "TEST_HI"; + + var outfit = CreateChild(root, "Outfit"); + var outfit_armature = CreateChild(outfit, "armature"); + var outfit_hips = CreateChild(outfit_armature, "pre_TEST_HI2.suf"); // Make it a little bit different name to confirm it matches the current implementation + + var outfit_mama = outfit_armature.AddComponent(); + outfit_mama.mergeTarget = new AvatarObjectReference(); + outfit_mama.mergeTarget.referencePath = RuntimeUtil.RelativePath(root, root_hips.parent.gameObject); + outfit_mama.LockMode = ArmatureLockMode.BaseToMerge; + + outfit_mama.InferPrefixSuffix(); + + Assert.AreEqual("pre_", outfit_mama.prefix); + Assert.AreEqual("2.suf", outfit_mama.suffix); + } + + [Test] + public void TestSameHipsName_Fail() + { + var root = CreateCommonPrefab("shapell.fbx"); +#if MA_VRCSDK3_AVATARS + root.AddComponent(); +#endif + var root_hips = root.GetComponent().GetBoneTransform(HumanBodyBones.Hips); + root_hips.name = "TE_HIPS_ST"; + + var outfit = CreateChild(root, "Outfit"); + var outfit_armature = CreateChild(outfit, "armature"); + var outfit_hips = CreateChild(outfit_armature, "pre_TE_HIPS_ST.suf"); + + var outfit_mama = outfit_armature.AddComponent(); + outfit_mama.mergeTarget = new AvatarObjectReference(); + outfit_mama.mergeTarget.referencePath = RuntimeUtil.RelativePath(root, root_hips.parent.gameObject); + outfit_mama.LockMode = ArmatureLockMode.BaseToMerge; + + outfit_mama.InferPrefixSuffix(); + + // Current(v1.10.x) InferPrefixSuffix fail to infer prefix/suffix when avatar has unique prefix/suffix and outfit has their name + Assert.AreNotEqual("pre_", outfit_mama.prefix); + Assert.AreNotEqual(".suf", outfit_mama.suffix); + } +} diff --git a/UnitTests~/EasySetupOutfit/InferPrefixSuffixTest.cs.meta b/UnitTests~/EasySetupOutfit/InferPrefixSuffixTest.cs.meta new file mode 100644 index 00000000..8fd3b1c4 --- /dev/null +++ b/UnitTests~/EasySetupOutfit/InferPrefixSuffixTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 426df05704d87424baeb85496181868d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnitTests~/EasySetupOutfit/SetupOutfitRenameTest.cs b/UnitTests~/EasySetupOutfit/SetupOutfitRenameTest.cs new file mode 100644 index 00000000..36d85f34 --- /dev/null +++ b/UnitTests~/EasySetupOutfit/SetupOutfitRenameTest.cs @@ -0,0 +1,71 @@ +using modular_avatar_tests; +using nadena.dev.modular_avatar.core; +using nadena.dev.modular_avatar.core.editor; +using NUnit.Framework; +using UnityEngine; + +public class SetupOutfitRenameTest : TestBase +{ + [Test] + public void TestSetupHumanoidOutfit() + { + var root = CreateCommonPrefab("shapell.fbx"); +#if MA_VRCSDK3_AVATARS + root.AddComponent(); +#endif + var root_chest = root.GetComponent().GetBoneTransform(HumanBodyBones.Chest); + + var outfit = CreateCommonPrefab("shapell.fbx"); + outfit.transform.SetParent(root.transform); + var outfit_chest = outfit.GetComponent().GetBoneTransform(HumanBodyBones.Chest); + outfit_chest.name = "c"; + + SetupOutfit.SetupOutfitUI(outfit); + + Assert.AreEqual(root_chest.name, outfit_chest.name); + } + + [Test] + public void TestSetupUpperChestOutfit() + { + var root = CreateCommonPrefab("shapell.fbx"); +#if MA_VRCSDK3_AVATARS + root.AddComponent(); +#endif + var root_armature = root.GetComponent().GetBoneTransform(HumanBodyBones.Hips).parent.gameObject; + var root_chest = root.GetComponent().GetBoneTransform(HumanBodyBones.Chest).gameObject; + + var outfit = CreateChild(root, "Outfit"); + var outfit_armature = CreateChild(outfit, "armature"); + var outfit_hips = CreateChild(outfit_armature, "hips"); + var outfit_spine = CreateChild(outfit_hips, "spine"); + var outfit_chest = CreateChild(outfit_spine, "chest"); + var outfit_upperchest = CreateChild(outfit_chest, "upperchest"); + + SetupOutfit.SetupOutfitUI(outfit); + + Assert.AreSame(root_armature, outfit_armature.GetComponent().mergeTargetObject); + Assert.AreSame(root_chest, outfit_upperchest.GetComponent().mergeTargetObject); + } + + [Test] + public void TestSetupSetupedOutfit() + { + var root = CreateCommonPrefab("shapell.fbx"); +#if MA_VRCSDK3_AVATARS + root.AddComponent(); +#endif + var root_hips = root.GetComponent().GetBoneTransform(HumanBodyBones.Hips); + var root_armature = root.GetComponent().GetBoneTransform(HumanBodyBones.Hips).parent.gameObject; + + var outfit = CreateChild(root, "Outfit"); + var outfit_armature = CreateChild(outfit, "armature"); + var outfit_hips = CreateChild(outfit_armature, "HIP"); + outfit_armature.AddComponent(); + + SetupOutfit.SetupOutfitUI(outfit); + + Assert.AreEqual(root_armature, outfit_armature.GetComponent().mergeTargetObject); + Assert.AreEqual(root_hips.name, outfit_hips.name); + } +} diff --git a/UnitTests~/EasySetupOutfit/SetupOutfitRenameTest.cs.meta b/UnitTests~/EasySetupOutfit/SetupOutfitRenameTest.cs.meta new file mode 100644 index 00000000..38a7ce2f --- /dev/null +++ b/UnitTests~/EasySetupOutfit/SetupOutfitRenameTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 89b8a54f81c4e7244a858b30825de67c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: