using System; using System.Linq; using System.Collections.Generic; using UnityEditor; using UnityEngine; using System.IO; using System.Xml.Linq; using Wwise.Wooduan.Components; using Debug = UnityEngine.Debug; namespace Wwise.Wooduan.Tools { internal class AkWooduanAnimationBackup : AkWooduanAssetSearcher { #region Fields private static AkWooduanAnimationBackup _instance; internal static AkWooduanAnimationBackup Instance { get { if (!_instance) { _instance = CreateInstance(); } return _instance; } } protected override string DefaultXmlPath { get { return AkWooduanScriptingTools.CombinePath(XmlDocDirectory, "AnimationEvents.xml"); } } #endregion #region Export internal void Export() { CleanUp(); var fileName = EditorUtility.SaveFilePanel("Export to", XmlDocDirectory, "AnimationEvents.xml", "xml"); if (string.IsNullOrEmpty(fileName)) return; if (IncludeA) FindFiles(ParseAnimation, "Exporting animation clips...", "*.anim"); if (IncludeB) FindFiles(ParseModel, "Exporting FBX files...", "*.fbx"); AkWooduanScriptingTools.WriteXml(fileName, XRoot); EditorUtility.DisplayDialog("Process Finished!", string.Format("Found {0} animation events in {1} clips!", EditedCount, TotalCount), "OK"); } internal void ParseAnimation(string assetPath) { var clip = AssetDatabase.LoadAssetAtPath(assetPath); if (clip == null) return; var xClip = ParseClip(clip, assetPath); if (xClip.HasElements) { XRoot.Add(xClip); TotalCount++; } } // export individual animation clip assets private XElement ParseClip(AnimationClip clip, string assetPath) { var xClip = new XElement("Animation"); xClip.SetAttributeValue("AssetPath", assetPath); foreach (var evt in clip.events) { // only export AkWooduan events if (IsAkWooduanAnimationEvent(evt)) xClip.Add(ParseEvent(evt)); } return xClip; } internal void ParseModel(string assetPath) { var modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter; if (modelImporter == null) return; var xModel = new XElement("Model"); foreach (var clip in modelImporter.clipAnimations) { var xClip = ParseModelClip(clip); if (xClip.HasElements) { xModel.Add(xClip); TotalCount++; } } if (xModel.HasElements) { xModel.SetAttributeValue("AssetPath", assetPath); XRoot.Add(xModel); } } // export animation clips embedded in models private XElement ParseModelClip(ModelImporterClipAnimation clip) { var xClip = new XElement("Clip"); xClip.SetAttributeValue("Name", clip.name); foreach (var evt in clip.events) { if (IsAkWooduanAnimationEvent(evt)) xClip.Add(ParseEvent(evt)); } return xClip; } private XElement ParseEvent(AnimationEvent evt) { EditedCount++; var ae = new XElement("AnimationEvent"); ae.SetAttributeValue("Function", evt.functionName); ae.SetAttributeValue("AudioEvent", evt.stringParameter); ae.SetAttributeValue("Time", evt.time); return ae; } #endregion #region Import internal void Import() { ImportedXmlPath = EditorUtility.OpenFilePanel("Import from", XmlDocDirectory, "xml"); if (string.IsNullOrEmpty(ImportedXmlPath) || !ReadXmlData(ImportedXmlPath)) return; if (IncludeA) ImportClips(false); if (IncludeB) ImportModels(false); EditorUtility.DisplayProgressBar("Saving", "Overwriting assets...(might take a few minutes)", 1f); AssetDatabase.SaveAssets(); EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("Process Finished!", string.Format("Updated {0} animation clips out of {1}", EditedCount, TotalCount), "OK"); } private void ImportClips(bool isCompare) { var xClips = XRoot.Elements("Animation").ToList(); try { for (var i = 0; i < xClips.Count; i++) { var assetPath = AkWooduanScriptingTools.GetXmlAttribute(xClips[i], "AssetPath"); if (!assetPath.Contains(SearchPath)) continue; if (isCompare) CompareAnimationClip(xClips[i]); else ImportClip(xClips[i]); if (EditorUtility.DisplayCancelableProgressBar("Processing clips", assetPath , (i + 1f) / TotalCount)) break; } } catch (Exception e) { Debug.LogException(e); EditorUtility.ClearProgressBar(); } } // import individual animation clip assets internal bool ImportClip(XElement xClip) { var assetPath = AkWooduanScriptingTools.GetXmlAttribute(xClip, "AssetPath"); var clip = AssetDatabase.LoadAssetAtPath(assetPath); if (!clip) return false; var modified = false; // find non audio events to initialize var events = clip.events.Where(evt => !IsAkWooduanAnimationEvent(evt)).ToList(); ImportEvents(events, xClip.Elements()); // event count is different, data must have changed if (events.Count != clip.events.Length) { AnimationUtility.SetAnimationEvents(clip, events.ToArray()); modified = true; } // sort events by time for comparison events = events.OrderBy(e => e.time).ToList(); if (events.Where((t, i) => !CompareAnimationEvent(clip.events[i], t)).Any()) { AnimationUtility.SetAnimationEvents(clip, events.ToArray()); modified = true; } return modified; } private void ImportModels(bool isCompare) { var xModels = XRoot.Elements("Model").ToList(); try { for (var i = 0; i < xModels.Count; i++) { var assetPath = AkWooduanScriptingTools.GetXmlAttribute(xModels[i], "AssetPath"); if (!assetPath.Contains(SearchPath)) continue; if (isCompare) CompareModel(xModels[i]); else ImportModel(xModels[i]); if (EditorUtility.DisplayCancelableProgressBar("Processing models", assetPath , (i + 1f) / TotalCount)) break; } } catch (Exception e) { Debug.LogException(e); EditorUtility.ClearProgressBar(); } } internal bool ImportModel(XElement xModel) { var assetPath = AkWooduanScriptingTools.GetXmlAttribute(xModel, "AssetPath"); var modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter; if (modelImporter == null) return false; var modified = false; // model clips can only be replaced as a whole, so creating a new list and populate with clips var newClips = new List(); foreach (var clip in modelImporter.clipAnimations) { var clipName = clip.name; // find corresponding node by clip name var xClip = xModel.Elements().FirstOrDefault(x => clipName == AkWooduanScriptingTools.GetXmlAttribute(x, "Name")); if (xClip != null && ImportModelClip(clip, xClip.Elements("AnimationEvent"))) modified = true; newClips.Add(clip); } if (modified) { modelImporter.clipAnimations = newClips.ToArray(); EditorUtility.SetDirty(modelImporter); AssetDatabase.ImportAsset(assetPath); } return modified; } // import clips embedded in models private bool ImportModelClip(ModelImporterClipAnimation clip, IEnumerable xEvents) { var events = clip.events.Where(evt => !IsAkWooduanAnimationEvent(evt)).ToList(); ImportEvents(events, xEvents); if (events.Count != clip.events.Length) { EditedCount++; clip.events = events.ToArray(); return true; } events = events.OrderBy(e => e.time).ToList(); if (events.Where((t, i) => !CompareAnimationEvent(clip.events[i], t)).Any()) { EditedCount++; clip.events = events.ToArray(); return true; } return false; } private void ImportEvents(ICollection events, IEnumerable xEvents) { foreach (var e in xEvents) { TotalCount++; var animEvent = new AnimationEvent { time = AkWooduanScriptingTools.StringToFloat(AkWooduanScriptingTools.GetXmlAttribute(e, "Time")), stringParameter = AkWooduanScriptingTools.GetXmlAttribute(e, "AudioEvent"), functionName = AkWooduanScriptingTools.GetXmlAttribute(e, "Function") }; events.Add(animEvent); } } private static bool IsAkWooduanAnimationEvent(AnimationEvent animationEvent) { return typeof(AnimationSound).GetMethods().Where(method => method.IsPublic).Any(method => animationEvent.functionName == method.Name); } #endregion #region Compare internal void Compare() { CleanUp(); ImportedXmlPath = EditorUtility.OpenFilePanel("Compare with", XmlDocDirectory, "xml"); if (string.IsNullOrEmpty(ImportedXmlPath) || !ReadXmlData(ImportedXmlPath)) return; if (IncludeA) ImportClips(true); if (IncludeB) ImportModels(true); AsAnimationEventCompare.ShowWindow(); EditorUtility.ClearProgressBar(); } private void CompareModel(XElement xModel) { var assetPath = AkWooduanScriptingTools.GetXmlAttribute(xModel, "AssetPath"); var modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter; if (modelImporter == null) return; foreach (var clip in modelImporter.clipAnimations) { var clipName = clip.name; // find corresponding node by clip name var xClip = xModel.Elements().FirstOrDefault(x => clipName == AkWooduanScriptingTools.GetXmlAttribute(x, "Name")); if (xClip != null) CompareClip(assetPath, xClip, clip.events); } } // wrapper for getting events from clip private void CompareAnimationClip(XElement xClip) { var assetPath = AkWooduanScriptingTools.GetXmlAttribute(xClip, "AssetPath"); var clip = AssetDatabase.LoadAssetAtPath(assetPath); CompareClip(assetPath, xClip, clip.events); } // actual place where comparison is done private void CompareClip(string assetPath, XElement xClip, AnimationEvent[] clipEvents) { var xEvents = xClip.Elements("AnimationEvent"); var events = clipEvents.Where(evt => !IsAkWooduanAnimationEvent(evt)).ToList(); ImportEvents(events, xEvents); var data = new ComponentComparisonData { AssetPath = assetPath, ComponentData = xClip, BackupStatus = ComponentBackupStatus.Unhandled }; // no events are found if (events.Count > clipEvents.Length && clipEvents.Length == 0) AsCompareWindow.MissingComponents.Add(data); else { events = events.OrderBy(e => e.time).ToList(); if (events.Where((t, i) => !CompareAnimationEvent(clipEvents[i], t)).Any()) AsCompareWindow.ModifiedComponents.Add(data); } } // check if events are equal private static bool CompareAnimationEvent(AnimationEvent a, AnimationEvent b) { return Math.Abs(a.time - b.time) < 0.01f && a.stringParameter == b.stringParameter && a.functionName == b.functionName; } #endregion #region Locate private XElement FindAssetNode(string assetPath, bool createIfMissing) { ReadXmlData(); // get the right type of asset var nodeName = assetPath.EndsWith(".anim") ? "Animation" : "Model"; var xAsset = XRoot.Elements(nodeName).FirstOrDefault(x => assetPath == AkWooduanScriptingTools.GetXmlAttribute(x, "AssetPath")); if (xAsset == null && createIfMissing) { xAsset = new XElement(nodeName); xAsset.SetAttributeValue("AssetPath", assetPath); XRoot.Add(xAsset); } return xAsset; } private static XElement FindClipNodeFromModel(XElement xModel, string clipName) { return xModel.Elements().FirstOrDefault(x => AkWooduanScriptingTools.GetXmlAttribute(x, "Name") == clipName); } #endregion #region Update private ModelImporterClipAnimation GetModelClipFromXml(string assetPath, XElement xClip) { var modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter; if (!modelImporter) return null; var clipName = AkWooduanScriptingTools.GetXmlAttribute(xClip, "Name"); return modelImporter.clipAnimations.FirstOrDefault(clip => clip.name == clipName); } private void UpdateXmlFromClip(ComponentComparisonData data) { var xAsset = FindAssetNode(data.AssetPath, true); var clipName = AkWooduanScriptingTools.GetXmlAttribute(data.ComponentData, "Name"); if (string.IsNullOrEmpty(clipName)) { var clip = AssetDatabase.LoadAssetAtPath(data.AssetPath); var xTemp = ParseClip(clip, data.AssetPath); if (XNode.DeepEquals(xTemp, xAsset)) return; xAsset.ReplaceWith(xTemp); } else { var xClip = FindClipNodeFromModel(xAsset, clipName); var clip = GetModelClipFromXml(data.AssetPath, xClip); var xTemp = ParseModelClip(clip); if (XNode.DeepEquals(xTemp, xClip)) return; xClip.ReplaceWith(xTemp); } AkWooduanScriptingTools.WriteXml(DefaultXmlPath, XRoot); } #endregion #region Revert private void RevertClipToXml(ComponentComparisonData data) { var xAsset = FindAssetNode(data.AssetPath, true); var clipName = AkWooduanScriptingTools.GetXmlAttribute(data.ComponentData, "Name"); if (string.IsNullOrEmpty(clipName)) ImportClip(xAsset); else ImportModel(xAsset); } #endregion #region Remove internal void RemoveAll() { ReadXmlData(); if (IncludeA) FindFiles(RemoveClip, "Removing animation clips...", "*.anim"); if (IncludeB) FindFiles(RemoveModel, "Removing FBX files...", "*.fbx"); AssetDatabase.SaveAssets(); EditorUtility.DisplayDialog("Process Finished!", "Removed " + EditedCount + " animation events in " + TotalCount + " clips and models!", "OK"); } internal void RemoveClip(string assetPath) { var clip = AssetDatabase.LoadAssetAtPath(assetPath); if (!clip || clip.events.Length == 0) return; var toBeRemained = clip.events.Where(animationEvent => !IsAkWooduanAnimationEvent(animationEvent)).ToArray(); TotalCount++; if (toBeRemained.Length == clip.events.Length) return; clip.events = toBeRemained; EditedCount++; EditorUtility.SetDirty(clip); } internal void RemoveModel(string assetPath) { var modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter; if (modelImporter == null) return; var modified = false; var newClips = new List(); TotalCount++; foreach (var clip in modelImporter.clipAnimations) { if (clip.events.Length == 0) continue; var toBeRemained = clip.events.Where(animationEvent => !IsAkWooduanAnimationEvent(animationEvent)).ToArray(); if (toBeRemained.Length == clip.events.Length) continue; clip.events = toBeRemained; modified = true; newClips.Add(clip); } if (modified) { modelImporter.clipAnimations = newClips.ToArray(); AssetDatabase.ImportAsset(assetPath); EditedCount++; } } private void RemoveClipXml(ComponentComparisonData data) { ReadXmlData(); var xAsset = FindAssetNode(data.AssetPath, false); if (XNode.DeepEquals(xAsset, data.ComponentData)) xAsset.Remove(); else { var xComponent = xAsset.Elements().FirstOrDefault(x => XNode.DeepEquals(x, data.ComponentData)); if (xComponent != null) AkWooduanScriptingTools.RemoveComponentXml(xComponent); } AkWooduanScriptingTools.WriteXml(DefaultXmlPath, XRoot); } #endregion private class AsAnimationEventCompare : AsCompareWindow { internal static void ShowWindow() { var window = GetWindow(); window.position = new Rect(500, 300, 700, 500); window.titleContent = new GUIContent("Compare Animation Events"); } protected override void DisplayData(ComponentComparisonData data) { var line = string.Format("{0} ({1})", Path.GetFileNameWithoutExtension(data.AssetPath), data.BackupStatus); var clipName = AkWooduanScriptingTools.GetXmlAttribute(data.ComponentData, "Name"); if (!string.IsNullOrEmpty(clipName)) line = clipName + ": " + line; if (GUILayout.Button(line, GUI.skin.label)) AsXmlInfo.Init(data.ComponentData); } protected override void DrawComponentList(List dataList, string description, ref Vector2 scrollPosition) { if (dataList.Count > 0) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(description + " Clips", EditorStyles.boldLabel); if (GUILayout.Button("Save All", GUILayout.Width(110))) SaveAll(dataList); if (GUILayout.Button("Revert All", GUILayout.Width(110))) RevertAll(dataList); if (GUILayout.Button("Remove All", GUILayout.Width(110))) RemoveAll(dataList); EditorGUILayout.EndHorizontal(); using (new EditorGUILayout.VerticalScope(GUI.skin.box)) { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(150)); foreach (var data in dataList) { EditorGUILayout.BeginHorizontal(); DisplayData(data); if (data.BackupStatus != ComponentBackupStatus.Removed) { GUI.contentColor = Color.yellow; if (GUILayout.Button("Locate", EditorStyles.toolbarButton, GUILayout.Width(50))) LocateComponent(data); GUI.contentColor = Color.green; if (dataList != MissingComponents && data.BackupStatus != ComponentBackupStatus.Saved && GUILayout.Button("Save", EditorStyles.toolbarButton, GUILayout.Width(50))) { SaveComponent(data); data.BackupStatus = ComponentBackupStatus.Saved; AssetDatabase.SaveAssets(); } GUI.contentColor = Color.magenta; if (data.BackupStatus != ComponentBackupStatus.Reverted && GUILayout.Button("Revert", EditorStyles.toolbarButton, GUILayout.Width(50))) { RevertComponent(data); data.BackupStatus = ComponentBackupStatus.Reverted; AssetDatabase.SaveAssets(); } GUI.contentColor = Color.red; if (GUILayout.Button("Remove", EditorStyles.toolbarButton, GUILayout.Width(50))) { RemoveComponent(data); data.BackupStatus = ComponentBackupStatus.Removed; AssetDatabase.SaveAssets(); } GUI.contentColor = Color.white; } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); } } else EditorGUILayout.LabelField("No " + description + " Components Found", EditorStyles.boldLabel); EditorGUILayout.Separator(); } protected override void LocateComponent(ComponentComparisonData data) { #if UNITY_2018_1_OR_NEWER EditorApplication.ExecuteMenuItem("Window/Animation/Animation"); #else EditorApplication.ExecuteMenuItem("Window/Animation"); #endif var clip = AssetDatabase.LoadAssetAtPath(data.AssetPath); if (clip) Selection.activeObject = clip; } private static void SaveComponent(ComponentComparisonData data) { Instance.UpdateXmlFromClip(data); } private static void RevertComponent(ComponentComparisonData data) { Instance.RevertClipToXml(data); } protected override void RemoveComponent(ComponentComparisonData data) { Instance.RemoveClipXml(data); } private static void SaveAll(IEnumerable dataList) { foreach (var data in dataList) { SaveComponent(data); data.BackupStatus = ComponentBackupStatus.Saved; } AssetDatabase.SaveAssets(); } private static void RevertAll(IEnumerable dataList) { foreach (var data in dataList) { RevertComponent(data); data.BackupStatus = ComponentBackupStatus.Reverted; } AssetDatabase.SaveAssets(); } } } }