/****************************************************************************** * Spine Runtimes License Agreement * Last updated July 28, 2023. Replaces all prior versions. * * Copyright (c) 2013-2023, Esoteric Software LLC * * Integration of the Spine Runtimes into software or otherwise creating * derivative works of the Spine Runtimes is permitted under the terms and * conditions of Section 2 of the Spine Editor License Agreement: * http://esotericsoftware.com/spine-editor-license * * Otherwise, it is permitted to integrate the Spine Runtimes into software or * otherwise create derivative works of the Spine Runtimes (collectively, * "Products"), provided that each user of the Products must obtain their own * Spine Editor license and redistribution of the Products in any form must * include this license and copyright notice. * * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ #pragma warning disable 0219 #pragma warning disable 0618 // for 3.7 branch only. Avoids "PreferenceItem' is obsolete: '[PreferenceItem] is deprecated. Use [SettingsProvider] instead." // Original contribution by: Mitch Thompson #define SPINE_SKELETONMECANIM #if UNITY_2017_2_OR_NEWER #define NEWPLAYMODECALLBACKS #endif #if UNITY_2018 || UNITY_2019 || UNITY_2018_3_OR_NEWER #define NEWHIERARCHYWINDOWCALLBACKS #endif #if UNITY_2018_3_OR_NEWER #define NEW_PREFERENCES_SETTINGS_PROVIDER #endif #if UNITY_2017_1_OR_NEWER #define BUILT_IN_SPRITE_MASK_COMPONENT #endif #if UNITY_2020_2_OR_NEWER #define HAS_ON_POSTPROCESS_PREFAB #endif using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using UnityEditor; using UnityEngine; namespace Spine.Unity.Editor { using EventType = UnityEngine.EventType; // Analysis disable once ConvertToStaticType [InitializeOnLoad] public partial class SpineEditorUtilities : AssetPostprocessor { public static string editorPath = ""; public static string editorGUIPath = ""; public static bool initialized; private static List texturesWithoutMetaFile = new List(); public static void OnTextureImportedFirstTime (string texturePath) { texturesWithoutMetaFile.Add(texturePath); } // Auto-import entry point for textures void OnPreprocessTexture () { #if UNITY_2018_1_OR_NEWER bool customTextureSettingsExist = !assetImporter.importSettingsMissing; #else bool customTextureSettingsExist = System.IO.File.Exists(assetImporter.assetPath + ".meta"); #endif if (!customTextureSettingsExist) { texturesWithoutMetaFile.Add(assetImporter.assetPath); } } // Auto-import post process entry point for all assets static void OnPostprocessAllAssets (string[] imported, string[] deleted, string[] moved, string[] movedFromAssetPaths) { if (imported.Length == 0) return; // we copy the list here to prevent nested calls to OnPostprocessAllAssets() triggering a Clear() of the list // in the middle of execution. List texturesWithoutMetaFileCopy = new List(texturesWithoutMetaFile); AssetUtility.HandleOnPostprocessAllAssets(imported, texturesWithoutMetaFileCopy); texturesWithoutMetaFile.Clear(); } #if HAS_ON_POSTPROCESS_PREFAB // Post process prefabs for setting the MeshFilter to not cause constant Prefab override changes. void OnPostprocessPrefab (GameObject g) { if (SpineBuildProcessor.isBuilding) return; SetupSpinePrefabMesh(g, context); } public static bool SetupSpinePrefabMesh (GameObject g, UnityEditor.AssetImporters.AssetImportContext context) { Dictionary nameUsageCount = new Dictionary(); bool wasModified = false; SkeletonRenderer[] skeletonRenderers = g.GetComponentsInChildren(true); foreach (SkeletonRenderer renderer in skeletonRenderers) { wasModified = true; MeshFilter meshFilter = renderer.GetComponent(); if (meshFilter == null) meshFilter = renderer.gameObject.AddComponent(); renderer.EditorUpdateMeshFilterHideFlags(); renderer.Initialize(true, true); renderer.LateUpdateMesh(); Mesh mesh = meshFilter.sharedMesh; if (mesh == null) continue; string meshName = string.Format("Skeleton Prefab Mesh [{0}]", renderer.name); if (nameUsageCount.ContainsKey(meshName)) { nameUsageCount[meshName]++; meshName = string.Format("Skeleton Prefab Mesh [{0} ({1})]", renderer.name, nameUsageCount[meshName]); } else { nameUsageCount.Add(meshName, 0); } mesh.name = meshName; mesh.hideFlags = HideFlags.None; if (context != null) context.AddObjectToAsset(meshFilter.sharedMesh.name, meshFilter.sharedMesh); } return wasModified; } public static bool CleanupSpinePrefabMesh (GameObject g) { bool wasModified = false; SkeletonRenderer[] skeletonRenderers = g.GetComponentsInChildren(true); foreach (SkeletonRenderer renderer in skeletonRenderers) { MeshFilter meshFilter = renderer.GetComponent(); if (meshFilter != null) { if (meshFilter.sharedMesh) { wasModified = true; meshFilter.sharedMesh = null; meshFilter.hideFlags = HideFlags.None; } } } return wasModified; } #endif #region Initialization static SpineEditorUtilities () { EditorApplication.delayCall += Initialize; // delayed so that AssetDatabase is ready. } static void Initialize () { // Note: Preferences need to be loaded when changing play mode // to initialize handle scale correctly. #if !NEW_PREFERENCES_SETTINGS_PROVIDER Preferences.Load(); #else SpinePreferences.Load(); #endif if (EditorApplication.isPlayingOrWillChangePlaymode) return; string[] assets; string assetPath; assets = AssetDatabase.FindAssets("t:texture icon-subMeshRenderer", null); if (assets.Length > 0) { assetPath = AssetDatabase.GUIDToAssetPath(assets[0]); editorGUIPath = Path.GetDirectoryName(assetPath).Replace('\\', '/'); } assets = AssetDatabase.FindAssets("t:script SpineEditorUtilities", null); if (assets.Length > 0) { assetPath = AssetDatabase.GUIDToAssetPath(assets[0]); editorPath = Path.GetDirectoryName(assetPath).Replace('\\', '/'); if (string.IsNullOrEmpty(editorGUIPath)) editorGUIPath = editorPath.Replace("/Utility", "/GUI"); } if (string.IsNullOrEmpty(editorGUIPath)) return; Icons.Initialize(); // Drag and Drop #if UNITY_2019_1_OR_NEWER SceneView.duringSceneGui -= DragAndDropInstantiation.SceneViewDragAndDrop; SceneView.duringSceneGui += DragAndDropInstantiation.SceneViewDragAndDrop; #else SceneView.onSceneGUIDelegate -= DragAndDropInstantiation.SceneViewDragAndDrop; SceneView.onSceneGUIDelegate += DragAndDropInstantiation.SceneViewDragAndDrop; #endif #if UNITY_2021_2_OR_NEWER DragAndDrop.RemoveDropHandler(HierarchyHandler.HandleDragAndDrop); DragAndDrop.AddDropHandler(HierarchyHandler.HandleDragAndDrop); #else EditorApplication.hierarchyWindowItemOnGUI -= HierarchyHandler.HandleDragAndDrop; EditorApplication.hierarchyWindowItemOnGUI += HierarchyHandler.HandleDragAndDrop; #endif // Hierarchy Icons #if NEWPLAYMODECALLBACKS EditorApplication.playModeStateChanged -= HierarchyHandler.IconsOnPlaymodeStateChanged; EditorApplication.playModeStateChanged += HierarchyHandler.IconsOnPlaymodeStateChanged; HierarchyHandler.IconsOnPlaymodeStateChanged(PlayModeStateChange.EnteredEditMode); #else EditorApplication.playmodeStateChanged -= HierarchyHandler.IconsOnPlaymodeStateChanged; EditorApplication.playmodeStateChanged += HierarchyHandler.IconsOnPlaymodeStateChanged; HierarchyHandler.IconsOnPlaymodeStateChanged(); #endif // Data Refresh Edit Mode. // This prevents deserialized SkeletonData from persisting from play mode to edit mode. #if NEWPLAYMODECALLBACKS EditorApplication.playModeStateChanged -= DataReloadHandler.OnPlaymodeStateChanged; EditorApplication.playModeStateChanged += DataReloadHandler.OnPlaymodeStateChanged; DataReloadHandler.OnPlaymodeStateChanged(PlayModeStateChange.EnteredEditMode); #else EditorApplication.playmodeStateChanged -= DataReloadHandler.OnPlaymodeStateChanged; EditorApplication.playmodeStateChanged += DataReloadHandler.OnPlaymodeStateChanged; DataReloadHandler.OnPlaymodeStateChanged(); #endif if (SpineEditorUtilities.Preferences.textureImporterWarning) { IssueWarningsForUnrecommendedTextureSettings(); } initialized = true; } public static void ConfirmInitialization () { if (!initialized) Initialize(); } public static void IssueWarningsForUnrecommendedTextureSettings () { string[] atlasDescriptionGUIDs = AssetDatabase.FindAssets("t:textasset .atlas"); // Note: finds ".atlas.txt" but also ".atlas 1.txt" files. for (int i = 0; i < atlasDescriptionGUIDs.Length; ++i) { string atlasDescriptionPath = AssetDatabase.GUIDToAssetPath(atlasDescriptionGUIDs[i]); if (!atlasDescriptionPath.EndsWith(".atlas.txt")) continue; string texturePath = atlasDescriptionPath.Replace(".atlas.txt", ".png"); bool textureExists = IssueWarningsForUnrecommendedTextureSettings(texturePath); if (!textureExists) { texturePath = texturePath.Replace(".png", ".jpg"); textureExists = IssueWarningsForUnrecommendedTextureSettings(texturePath); } if (!textureExists) { continue; } } } public static void ReloadSkeletonDataAssetAndComponent (SkeletonRenderer component) { if (component == null) return; ReloadSkeletonDataAsset(component.skeletonDataAsset); ReinitializeComponent(component); } public static void ReloadSkeletonDataAssetAndComponent (SkeletonGraphic component) { if (component == null) return; ReloadSkeletonDataAsset(component.skeletonDataAsset); // Reinitialize. ReinitializeComponent(component); } public static void ClearSkeletonDataAsset (SkeletonDataAsset skeletonDataAsset) { skeletonDataAsset.Clear(); DataReloadHandler.ClearAnimationReferenceAssets(skeletonDataAsset); } public static void ReloadSkeletonDataAsset (SkeletonDataAsset skeletonDataAsset, bool clearAtlasAssets = true) { if (skeletonDataAsset == null) return; if (clearAtlasAssets) { foreach (AtlasAssetBase aa in skeletonDataAsset.atlasAssets) { if (aa != null) aa.Clear(); } } ClearSkeletonDataAsset(skeletonDataAsset); skeletonDataAsset.GetSkeletonData(true); DataReloadHandler.ReloadAnimationReferenceAssets(skeletonDataAsset); } public static void ReinitializeComponent (SkeletonRenderer component) { if (component == null) return; if (!SkeletonDataAssetIsValid(component.SkeletonDataAsset)) return; IAnimationStateComponent stateComponent = component as IAnimationStateComponent; AnimationState oldAnimationState = null; if (stateComponent != null) { oldAnimationState = stateComponent.AnimationState; } component.Initialize(true); // implicitly clears any subscribers if (oldAnimationState != null) { stateComponent.AnimationState.AssignEventSubscribersFrom(oldAnimationState); } if (stateComponent != null) { // Any set animation needs to be applied as well since it might set attachments, // having an effect on generated SpriteMaskMaterials below. stateComponent.AnimationState.Apply(component.skeleton); component.LateUpdate(); } #if BUILT_IN_SPRITE_MASK_COMPONENT SpineMaskUtilities.EditorAssignSpriteMaskMaterials(component); #endif component.LateUpdate(); } public static void ReinitializeComponent (SkeletonGraphic component) { if (component == null) return; if (!SkeletonDataAssetIsValid(component.SkeletonDataAsset)) return; component.Initialize(true); component.LateUpdate(); } public static bool SkeletonDataAssetIsValid (SkeletonDataAsset asset) { return asset != null && asset.GetSkeletonData(quiet: true) != null; } public static bool IssueWarningsForUnrecommendedTextureSettings (string texturePath) { TextureImporter texImporter = (TextureImporter)TextureImporter.GetAtPath(texturePath); if (texImporter == null) { return false; } int extensionPos = texturePath.LastIndexOf('.'); string materialPath = texturePath.Substring(0, extensionPos) + "_Material.mat"; Material material = AssetDatabase.LoadAssetAtPath(materialPath); if (material == null) return true; string errorMessage = null; if (MaterialChecks.IsTextureSetupProblematic(material, PlayerSettings.colorSpace, texImporter.sRGBTexture, texImporter.mipmapEnabled, texImporter.alphaIsTransparency, texturePath, materialPath, ref errorMessage)) { Debug.LogWarning(errorMessage, material); } return true; } #endregion public static class HierarchyHandler { static Dictionary skeletonRendererTable = new Dictionary(); static Dictionary skeletonUtilityBoneTable = new Dictionary(); static Dictionary boundingBoxFollowerTable = new Dictionary(); static Dictionary boundingBoxFollowerGraphicTable = new Dictionary(); #if NEWPLAYMODECALLBACKS internal static void IconsOnPlaymodeStateChanged (PlayModeStateChange stateChange) { #else internal static void IconsOnPlaymodeStateChanged () { #endif skeletonRendererTable.Clear(); skeletonUtilityBoneTable.Clear(); boundingBoxFollowerTable.Clear(); boundingBoxFollowerGraphicTable.Clear(); #if NEWHIERARCHYWINDOWCALLBACKS EditorApplication.hierarchyChanged -= IconsOnChanged; #else EditorApplication.hierarchyWindowChanged -= IconsOnChanged; #endif EditorApplication.hierarchyWindowItemOnGUI -= IconsOnGUI; if (!Application.isPlaying && Preferences.showHierarchyIcons) { #if NEWHIERARCHYWINDOWCALLBACKS EditorApplication.hierarchyChanged += IconsOnChanged; #else EditorApplication.hierarchyWindowChanged += IconsOnChanged; #endif EditorApplication.hierarchyWindowItemOnGUI += IconsOnGUI; IconsOnChanged(); } } internal static void IconsOnChanged () { skeletonRendererTable.Clear(); skeletonUtilityBoneTable.Clear(); boundingBoxFollowerTable.Clear(); boundingBoxFollowerGraphicTable.Clear(); SkeletonRenderer[] arr = Object.FindObjectsOfType(); foreach (SkeletonRenderer r in arr) skeletonRendererTable[r.gameObject.GetInstanceID()] = r.gameObject; SkeletonUtilityBone[] boneArr = Object.FindObjectsOfType(); foreach (SkeletonUtilityBone b in boneArr) skeletonUtilityBoneTable[b.gameObject.GetInstanceID()] = b; BoundingBoxFollower[] bbfArr = Object.FindObjectsOfType(); foreach (BoundingBoxFollower bbf in bbfArr) boundingBoxFollowerTable[bbf.gameObject.GetInstanceID()] = bbf; BoundingBoxFollowerGraphic[] bbfgArr = Object.FindObjectsOfType(); foreach (BoundingBoxFollowerGraphic bbf in bbfgArr) boundingBoxFollowerGraphicTable[bbf.gameObject.GetInstanceID()] = bbf; } internal static void IconsOnGUI (int instanceId, Rect selectionRect) { Rect r = new Rect(selectionRect); if (skeletonRendererTable.ContainsKey(instanceId)) { r.x = r.width - 15; r.width = 15; GUI.Label(r, Icons.spine); } else if (skeletonUtilityBoneTable.ContainsKey(instanceId)) { r.x -= 26; if (skeletonUtilityBoneTable[instanceId] != null) { if (skeletonUtilityBoneTable[instanceId].transform.childCount == 0) r.x += 13; r.y += 2; r.width = 13; r.height = 13; if (skeletonUtilityBoneTable[instanceId].mode == SkeletonUtilityBone.Mode.Follow) GUI.DrawTexture(r, Icons.bone); else GUI.DrawTexture(r, Icons.poseBones); } } else if (boundingBoxFollowerTable.ContainsKey(instanceId)) { r.x -= 26; if (boundingBoxFollowerTable[instanceId] != null) { if (boundingBoxFollowerTable[instanceId].transform.childCount == 0) r.x += 13; r.y += 2; r.width = 13; r.height = 13; GUI.DrawTexture(r, Icons.boundingBox); } } else if (boundingBoxFollowerGraphicTable.ContainsKey(instanceId)) { r.x -= 26; if (boundingBoxFollowerGraphicTable[instanceId] != null) { if (boundingBoxFollowerGraphicTable[instanceId].transform.childCount == 0) r.x += 13; r.y += 2; r.width = 13; r.height = 13; GUI.DrawTexture(r, Icons.boundingBox); } } } #if UNITY_2021_2_OR_NEWER internal static DragAndDropVisualMode HandleDragAndDrop (int dropTargetInstanceID, HierarchyDropFlags dropMode, Transform parentForDraggedObjects, bool perform) { SkeletonDataAsset skeletonDataAsset = DragAndDrop.objectReferences.Length == 0 ? null : DragAndDrop.objectReferences[0] as SkeletonDataAsset; if (skeletonDataAsset == null) return DragAndDropVisualMode.None; if (!perform) return DragAndDropVisualMode.Copy; GameObject dropTargetObject = UnityEditor.EditorUtility.InstanceIDToObject(dropTargetInstanceID) as GameObject; Transform dropTarget = dropTargetObject != null ? dropTargetObject.transform : null; Transform parent = dropTarget; int siblingIndex = 0; if (parent != null) { if (dropMode == HierarchyDropFlags.DropBetween) { parent = dropTarget.parent; siblingIndex = dropTarget ? dropTarget.GetSiblingIndex() + 1 : 0; } else if (dropMode == HierarchyDropFlags.DropAbove) { parent = dropTarget.parent; siblingIndex = dropTarget ? dropTarget.GetSiblingIndex() : 0; } } DragAndDropInstantiation.ShowInstantiateContextMenu(skeletonDataAsset, Vector3.zero, parent, siblingIndex); return DragAndDropVisualMode.Copy; } #else internal static void HandleDragAndDrop (int instanceId, Rect selectionRect) { // HACK: Uses EditorApplication.hierarchyWindowItemOnGUI. // Only works when there is at least one item in the scene. UnityEngine.Event current = UnityEngine.Event.current; EventType eventType = current.type; bool isDraggingEvent = eventType == EventType.DragUpdated; bool isDropEvent = eventType == EventType.DragPerform; UnityEditor.DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (isDraggingEvent || isDropEvent) { EditorWindow mouseOverWindow = EditorWindow.mouseOverWindow; if (mouseOverWindow != null) { // One, existing, valid SkeletonDataAsset Object[] references = UnityEditor.DragAndDrop.objectReferences; if (references.Length == 1) { SkeletonDataAsset skeletonDataAsset = references[0] as SkeletonDataAsset; if (skeletonDataAsset != null && skeletonDataAsset.GetSkeletonData(true) != null) { // Allow drag-and-dropping anywhere in the Hierarchy Window. // HACK: string-compare because we can't get its type via reflection. const string HierarchyWindow = "UnityEditor.SceneHierarchyWindow"; const string GenericDataTargetID = "target"; if (HierarchyWindow.Equals(mouseOverWindow.GetType().ToString(), System.StringComparison.Ordinal)) { if (isDraggingEvent) { UnityEngine.Object mouseOverTarget = UnityEditor.EditorUtility.InstanceIDToObject(instanceId); if (mouseOverTarget) DragAndDrop.SetGenericData(GenericDataTargetID, mouseOverTarget); // Note: do not call current.Use(), otherwise we get the wrong drop-target parent. } else if (isDropEvent) { GameObject parentGameObject = DragAndDrop.GetGenericData(GenericDataTargetID) as UnityEngine.GameObject; Transform parent = parentGameObject != null ? parentGameObject.transform : null; // when dragging into empty space in hierarchy below last node, last node would be parent. if (IsLastNodeInHierarchy(parent)) parent = null; DragAndDropInstantiation.ShowInstantiateContextMenu(skeletonDataAsset, Vector3.zero, parent, 0); UnityEditor.DragAndDrop.AcceptDrag(); current.Use(); return; } } } } } } } internal static bool IsLastNodeInHierarchy (Transform node) { if (node == null) return false; while (node.parent != null) { if (node.GetSiblingIndex() != node.parent.childCount - 1) return false; node = node.parent; } GameObject[] rootNodes = UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects(); bool isLastNode = (rootNodes.Length > 0 && rootNodes[rootNodes.Length - 1].transform == node); return isLastNode; } #endif } } public class SpineAssetModificationProcessor : UnityEditor.AssetModificationProcessor { static void OnWillCreateAsset (string assetName) { // Note: This method seems to be called from the main thread, // not from worker threads when Project Settings - Editor - Parallel Import is enabled. int endIndex = assetName.LastIndexOf(".meta"); string assetPath = endIndex < 0 ? assetName : assetName.Substring(0, endIndex); SpineEditorUtilities.OnTextureImportedFirstTime(assetPath); } } public class TextureModificationWarningProcessor : UnityEditor.AssetModificationProcessor { static string[] OnWillSaveAssets (string[] paths) { if (SpineEditorUtilities.Preferences.textureImporterWarning) { foreach (string path in paths) { if ((path != null) && (path.EndsWith(".png.meta", System.StringComparison.Ordinal) || path.EndsWith(".jpg.meta", System.StringComparison.Ordinal))) { string texturePath = System.IO.Path.ChangeExtension(path, null); // .meta removed string atlasPath = System.IO.Path.ChangeExtension(texturePath, "atlas.txt"); if (System.IO.File.Exists(atlasPath)) SpineEditorUtilities.IssueWarningsForUnrecommendedTextureSettings(texturePath); } } } return paths; } } public class AnimationWindowPreview { static System.Type animationWindowType; public static System.Type AnimationWindowType { get { if (animationWindowType == null) animationWindowType = System.Type.GetType("UnityEditor.AnimationWindow,UnityEditor"); return animationWindowType; } } public static UnityEngine.Object GetOpenAnimationWindow () { UnityEngine.Object[] openAnimationWindows = Resources.FindObjectsOfTypeAll(AnimationWindowType); return openAnimationWindows.Length == 0 ? null : openAnimationWindows[0]; } public static AnimationClip GetAnimationClip (UnityEngine.Object animationWindow) { if (animationWindow == null) return null; const BindingFlags bindingFlagsInstance = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; FieldInfo animEditorField = AnimationWindowType.GetField("m_AnimEditor", bindingFlagsInstance); PropertyInfo selectionProperty = animEditorField.FieldType.GetProperty("selection", bindingFlagsInstance); object animEditor = animEditorField.GetValue(animationWindow); if (animEditor == null) return null; object selection = selectionProperty.GetValue(animEditor, null); if (selection == null) return null; PropertyInfo animationClipProperty = selection.GetType().GetProperty("animationClip"); return animationClipProperty.GetValue(selection, null) as AnimationClip; } public static float GetAnimationTime (UnityEngine.Object animationWindow) { if (animationWindow == null) return 0.0f; const BindingFlags bindingFlagsInstance = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; FieldInfo animEditorField = AnimationWindowType.GetField("m_AnimEditor", bindingFlagsInstance); object animEditor = animEditorField.GetValue(animationWindow); System.Type animEditorFieldType = animEditorField.FieldType; PropertyInfo stateProperty = animEditorFieldType.GetProperty("state", bindingFlagsInstance); System.Type animWindowStateType = stateProperty.PropertyType; PropertyInfo timeProperty = animWindowStateType.GetProperty("currentTime", bindingFlagsInstance); object state = stateProperty.GetValue(animEditor, null); return (float)timeProperty.GetValue(state, null); } } }