Initial Commit

This commit is contained in:
jellejurre
2025-07-19 01:03:02 +02:00
commit e7904e3140
304 changed files with 22521 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b095e083c4f79b24aaf1e6460f3e35e8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,384 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.Animations;
using VRC.SDK3.Avatars.Components;
using System.Linq;
namespace DreadScripts.ReplaceMotion
{
public class ReplaceMotion : EditorWindow
{
static List<Motion> originalMotion = new List<Motion>();
private static bool hasEmptyState;
private static bool replacingEmptyState;
static bool[] replaceFields;
static Motion[] targetMotion;
static Motion emptyTargetMotion;
static readonly Dictionary<Motion, Motion> replaceValues = new Dictionary<Motion, Motion>();
private static VRCAvatarDescriptor mainAvatar;
private static AnimatorController mainController;
private static Vector2 scroll;
[MenuItem("DreadTools/Utility/Replace Motion")]
private static void showWindow()
{
GetWindow<ReplaceMotion>(false, "Replace Motion", true);
}
private void OnEnable()
{
if (!mainAvatar && !mainController)
{
mainAvatar = FindObjectOfType<VRCAvatarDescriptor>();
if (mainAvatar)
GetMotions(mainAvatar);
}
}
private void OnGUI()
{
GUIStyle labelButton = new GUIStyle(GUI.skin.button) { padding = new RectOffset(1, 1, 1, 1), margin = new RectOffset(), alignment = TextAnchor.MiddleCenter };
scroll = EditorGUILayout.BeginScrollView(scroll);
using (new GUILayout.HorizontalScope("box"))
{
EditorGUI.BeginChangeCheck();
Object dummy = CustomObjectField(new GUIContent("Target"), (Object)mainAvatar ?? mainController ?? null, true, true, out int resultType, typeof(VRCAvatarDescriptor), typeof(AnimatorController));
if (EditorGUI.EndChangeCheck())
{
switch (resultType)
{
case -1:
mainAvatar = null;
mainController = null;
break;
case 0:
mainAvatar = (VRCAvatarDescriptor)dummy;
mainController = null;
break;
case 1:
mainAvatar = null;
mainController = (AnimatorController)dummy;
break;
}
if (mainAvatar)
GetMotions(mainAvatar);
else if (mainController)
GetMotions(mainController);
}
}
EditorGUI.BeginDisabledGroup(!(mainAvatar || mainController));
if (GUILayout.Button("Replace"))
{
PopulateDictionary();
if (mainAvatar)
SetMotions(mainAvatar);
else
SetMotions(mainController);
AssetDatabase.SaveAssets();
Debug.Log("<color=green>[Replace Motion] </color>Done!");
if (mainAvatar)
GetMotions(mainAvatar);
else
GetMotions(mainController);
}
EditorGUI.EndDisabledGroup();
if (originalMotion.Count > 0 || hasEmptyState)
{
DrawSeperator();
if (hasEmptyState)
{
using (new GUILayout.HorizontalScope("box"))
{
EditorGUILayout.ObjectField(null, typeof(Motion), false);
GUILayout.Space(20);
GUILayout.Label("->", "boldlabel", GUILayout.Width(20));
GUILayout.Space(20);
if (!replacingEmptyState)
replacingEmptyState = GUILayout.Toggle(replacingEmptyState, new GUIContent("Replace", "Replace all instances of the original motion"), "button");
else
{
if (GUILayout.Button("X", labelButton, GUILayout.Width(20)))
replacingEmptyState = false;
emptyTargetMotion = (Motion)EditorGUILayout.ObjectField(emptyTargetMotion, typeof(Motion), true);
}
}
}
for (int i = 0; i < originalMotion.Count; i++)
{
using (new GUILayout.HorizontalScope("box"))
{
EditorGUILayout.ObjectField(originalMotion[i], typeof(Motion), false);
GUILayout.Space(20);
GUILayout.Label("->", "boldlabel", GUILayout.Width(20));
GUILayout.Space(20);
if (!replaceFields[i])
replaceFields[i] = GUILayout.Toggle(replaceFields[i],new GUIContent("Replace","Replace all instances of the original motion"), "button");
else
{
if (GUILayout.Button("X", labelButton, GUILayout.Width(20)))
replaceFields[i] = false;
targetMotion[i] = (Motion)EditorGUILayout.ObjectField(targetMotion[i], typeof(Motion), true);
}
}
}
}
EditorGUILayout.EndScrollView();
}
private static Object CustomObjectField(GUIContent label, Object displayObject, bool allowScene, bool checkComponents, out int resultTypeIndex, params System.Type[] validTypes)
{
//Special Cases
bool supportsController = false;
int controllerTypeIndex = -1;
if (checkComponents)
{
for (int i = 0; i < validTypes.Length; i++)
{
if (validTypes[i] == typeof(AnimatorController))
{
supportsController = true;
controllerTypeIndex = i;
break;
}
}
}
///////////////
EditorGUI.BeginChangeCheck();
Object dummy = EditorGUILayout.ObjectField(label, displayObject, typeof(Object), allowScene);
if (EditorGUI.EndChangeCheck())
{
if (!dummy)
{
resultTypeIndex = -1;
return null;
}
for (int i = 0; i < validTypes.Length; i++)
{
if (dummy.GetType() == validTypes[i])
{
resultTypeIndex = i;
return dummy;
}
}
if (checkComponents && dummy is GameObject go)
{
Component[] components = go.GetComponents<Component>();
for (int i = 0; i < components.Length; i++)
{
for (int j = 0; j < validTypes.Length; j++)
{
if (components[i].GetType() == validTypes[j])
{
resultTypeIndex = j;
return components[i];
}
//Special Cases
if (supportsController && (components[i] is Animator ani) && ani.runtimeAnimatorController)
{
resultTypeIndex = controllerTypeIndex;
return AssetDatabase.LoadAssetAtPath<AnimatorController>(AssetDatabase.GetAssetPath(ani.runtimeAnimatorController));
}
///////////////
}
}
}
string validTypesMessage = string.Join(", ", validTypes.Select(t => t.Name));
Debug.LogWarning("Field must be of Type: " + validTypesMessage);
}
resultTypeIndex = -2;
return dummy;
}
private void SetMotions(VRCAvatarDescriptor avatar)
{
IterateStates(avatar, SetMotions);
}
private void GetMotions(VRCAvatarDescriptor avatar)
{
GetStart();
IterateStates(avatar, s => GetMotions(s.motion));
GetFinal();
}
private void SetMotions(AnimatorController controller)
{
IterateStates(controller, SetMotions);
}
private void GetMotions(AnimatorController controller)
{
GetStart();
IterateStates(controller, s => GetMotions(s.motion));
GetFinal();
}
private void GetStart()
{
hasEmptyState = false;
emptyTargetMotion = null;
originalMotion.Clear();
}
private void GetFinal()
{
originalMotion = originalMotion.Distinct().ToList();
targetMotion = new Motion[originalMotion.Count];
replaceFields = new bool[originalMotion.Count];
}
private void GetMotions(BlendTree tree)
{
originalMotion.Add(tree);
for (int i = 0; i < tree.children.Length; i++)
{
GetMotions(tree.children[i].motion);
}
}
private void GetMotions(Motion motion)
{
if (!motion)
hasEmptyState = true;
else
{
if (motion is AnimationClip)
originalMotion.Add(motion);
else
{
if (motion is BlendTree tree)
{
GetMotions(tree);
}
}
}
}
private void SetMotions(AnimatorState state)
{
if (!state.motion)
{
if (replacingEmptyState)
state.motion = emptyTargetMotion;
}
else
{
if (replaceValues.ContainsKey(state.motion))
{
state.motion = replaceValues[state.motion];
EditorUtility.SetDirty(state);
}
else
{
if (state.motion is BlendTree tree)
{
SetMotions(tree);
}
}
}
}
private void SetMotions(BlendTree tree)
{
ChildMotion[] newMotions = tree.children;
for (int i = 0; i < newMotions.Length; i++)
{
if (replaceValues.ContainsKey(newMotions[i].motion))
newMotions[i].motion = replaceValues[newMotions[i].motion];
else
if (newMotions[i].motion is BlendTree subTree)
SetMotions(subTree);
}
tree.children = newMotions;
EditorUtility.SetDirty(tree);
}
private void PopulateDictionary()
{
replaceValues.Clear();
for (int i = 0; i < originalMotion.Count; i++)
{
if (!replaceFields[i])
replaceValues.Add(originalMotion[i], originalMotion[i]);
else
replaceValues.Add(originalMotion[i], targetMotion[i]);
}
}
public static void IterateStates(VRCAvatarDescriptor avatar, System.Action<AnimatorState> action)
{
HashSet<AnimatorController> visitedControllers = new HashSet<AnimatorController>();
foreach (var layer in avatar.baseAnimationLayers.Concat(avatar.specialAnimationLayers))
{
if (layer.animatorController)
{
AnimatorController controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(AssetDatabase.GetAssetPath(layer.animatorController));
if (controller && !visitedControllers.Contains(controller))
{
IterateStates(controller, action);
visitedControllers.Add(controller);
}
}
}
foreach (var runtimeController in avatar.GetComponentsInChildren<Animator>().Select(a => a.runtimeAnimatorController))
{
if (runtimeController)
{
AnimatorController controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(AssetDatabase.GetAssetPath(runtimeController));
if (controller && !visitedControllers.Contains(controller))
{
IterateStates(controller, action);
visitedControllers.Add(controller);
}
}
}
}
public static void IterateStates(AnimatorController controller, System.Action<AnimatorState> action)
{
foreach (var layer in controller.layers)
{
IterateStates(layer.stateMachine, action, true);
}
}
public static void IterateStates(AnimatorStateMachine machine, System.Action<AnimatorState> action, bool deep = true)
{
if (deep)
foreach (var subMachine in machine.stateMachines.Select(c => c.stateMachine))
{
IterateStates(subMachine, action);
}
foreach (var state in machine.states.Select(s => s.state))
{
action(state);
}
}
private static void DrawSeperator()
{
Rect r = EditorGUILayout.GetControlRect(GUILayout.Height(1 + 2));
r.height = 1;
r.y += 1;
r.x -= 2;
r.width += 6;
ColorUtility.TryParseHtmlString(EditorGUIUtility.isProSkin ? "#595959" : "#858585", out Color lineColor);
EditorGUI.DrawRect(r, lineColor);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20a6e7c15d5f4744681ced7247a029a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
{
"name": "com.dreadscripts.replacemotion.Editor",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 355ae8e34ea9b874a9965deb911d8a85
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

21
ReplaceMotion/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Dreadrith
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 38e2ba75221516844b3416a99546dbe5
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

20
ReplaceMotion/README.md Normal file
View File

@@ -0,0 +1,20 @@
# Replace Motion
Easily replace all instances of a motion in an Avatar or Controller with the desired motion.
![image](https://cdn.discordapp.com/attachments/1067972913594118214/1067972913988386867/unknown_4.png?ex=663437c8&is=6632e648&hm=5faf9a4ba98a093d3cccd7906daf6fee811e4fb40bbe446c5c449355d7291b13&)
Replace Motion will easily replace all instances of a motion in an Avatar or Controller with the desired motion.
Found under DreadTools > Utilities > Replace Motion
Window
-------
After specifying a target avatar or controller, the window will show you all the motions used. You can replace all instances of a motion with another motion of your choice.
Target:The Avatar or Animator Controller to replace motions in.
Motion List:
- Left Field: The original motion that's used in the target
- Right Field: the new motion that the original motion should be replaced with
### Thank you
If you enjoy Replace Motion, please consider [supporting me ♡](https://ko-fi.com/Dreadrith)!

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 575a36e847cb59c4488f4fc71997bec8
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
{
"name": "com.dreadscripts.replacemotion",
"displayName": "DreadScripts - ReplaceMotion",
"version": "1.0.1",
"description": "Easily replace all instances of a motion in an Avatar or Controller with the desired motion.",
"gitDependencies": {},
"vpmDependencies": {},
"author": {
"name": "Dreadrith",
"email": "dreadscripts@gmail.com",
"url": "https://www.dreadrith.com"
},
"legacyFolders": {},
"legacyFiles": {},
"type": "tool",
"unity": "2019.4"
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f83ab771cf8f96246b561c63d1a24535
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: