using UnityEngine; using UnityEditor; using UnityEditor.SceneManagement; using Core.SaveLoad; using System.Collections.Generic; using System.Linq; namespace Editor { /// /// Editor utility to migrate StateMachine components to SaveableStateMachine. /// public class StateMachineMigrationTool : EditorWindow { private Vector2 scrollPosition; private List foundStateMachines = new List(); private bool showPrefabs = true; private bool showScenes = true; [MenuItem("Tools/AppleHills/Migrate StateMachines to Saveable")] public static void ShowWindow() { var window = GetWindow("StateMachine Migration"); window.minSize = new Vector2(600, 400); window.Show(); } private void OnGUI() { EditorGUILayout.LabelField("StateMachine → SaveableStateMachine Migration", EditorStyles.boldLabel); EditorGUILayout.Space(); EditorGUILayout.HelpBox( "This tool will replace all StateMachine components with SaveableStateMachine.\n" + "All properties and references will be preserved.", MessageType.Info ); EditorGUILayout.Space(); // Scan options EditorGUILayout.LabelField("Scan Options:", EditorStyles.boldLabel); showPrefabs = EditorGUILayout.Toggle("Include Prefabs", showPrefabs); showScenes = EditorGUILayout.Toggle("Include Scenes", showScenes); EditorGUILayout.Space(); if (GUILayout.Button("Scan Project", GUILayout.Height(30))) { ScanProject(); } EditorGUILayout.Space(); // Display results if (foundStateMachines.Count > 0) { EditorGUILayout.LabelField($"Found {foundStateMachines.Count} StateMachine(s):", EditorStyles.boldLabel); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); foreach (var info in foundStateMachines) { // Set background color for migrated items Color originalBgColor = GUI.backgroundColor; if (info.isAlreadySaveable) { GUI.backgroundColor = new Color(0.5f, 1f, 0.5f); // Light green } EditorGUILayout.BeginHorizontal("box"); GUI.backgroundColor = originalBgColor; // Reset for content EditorGUILayout.LabelField(info.name, GUILayout.Width(200)); EditorGUILayout.LabelField(info.path, GUILayout.ExpandWidth(true)); // Ping button (for scene objects or prefabs) if (GUILayout.Button("Ping", GUILayout.Width(50))) { PingObject(info); } if (info.isAlreadySaveable) { // Green checkmark with bold style GUIStyle greenStyle = new GUIStyle(GUI.skin.label); greenStyle.normal.textColor = new Color(0f, 0.6f, 0f); greenStyle.fontStyle = FontStyle.Bold; EditorGUILayout.LabelField("✓ Migrated", greenStyle, GUILayout.Width(120)); } else { if (GUILayout.Button("Migrate", GUILayout.Width(80))) { if (MigrateSingle(info)) { // Refresh the list to show updated status ScanProject(); return; } } } EditorGUILayout.EndHorizontal(); GUI.backgroundColor = originalBgColor; // Final reset } EditorGUILayout.EndScrollView(); EditorGUILayout.Space(); int migrateCount = foundStateMachines.Count(sm => !sm.isAlreadySaveable); if (migrateCount > 0) { if (GUILayout.Button($"Migrate All ({migrateCount})", GUILayout.Height(40))) { if (EditorUtility.DisplayDialog( "Confirm Migration", $"This will migrate {migrateCount} StateMachine(s) to SaveableStateMachine.\n\nContinue?", "Yes, Migrate All", "Cancel")) { MigrateAll(); } } } } else { EditorGUILayout.HelpBox("Click 'Scan Project' to find StateMachine components.", MessageType.Info); } } private void ScanProject() { foundStateMachines.Clear(); // Find all prefabs if (showPrefabs) { string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab"); foreach (string guid in prefabGuids) { string path = AssetDatabase.GUIDToAssetPath(guid); GameObject prefab = AssetDatabase.LoadAssetAtPath(path); if (prefab != null) { // Use GetComponents to find Pixelplacement.StateMachine var components = prefab.GetComponentsInChildren(true); foreach (var component in components) { if (component == null) continue; var componentType = component.GetType(); if (componentType.Name == "StateMachine" && componentType.Namespace == "Pixelplacement") { bool isAlreadySaveable = component is AppleMachine; foundStateMachines.Add(new StateMachineInfo { name = component.gameObject.name, path = path, isPrefab = true, isAlreadySaveable = isAlreadySaveable, assetPath = path }); } } } } } // Find all in scenes if (showScenes) { string[] sceneGuids = AssetDatabase.FindAssets("t:Scene"); foreach (string guid in sceneGuids) { string scenePath = AssetDatabase.GUIDToAssetPath(guid); var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); var allComponents = GameObject.FindObjectsOfType(true); foreach (var component in allComponents) { if (component == null || component.gameObject.scene != scene) continue; var componentType = component.GetType(); if (componentType.Name == "StateMachine" && componentType.Namespace == "Pixelplacement") { bool isAlreadySaveable = component is AppleMachine; foundStateMachines.Add(new StateMachineInfo { name = component.gameObject.name, path = $"{scenePath} → {component.transform.GetHierarchyPath()}", isPrefab = false, isAlreadySaveable = isAlreadySaveable, assetPath = scenePath, gameObject = component.gameObject }); } } EditorSceneManager.CloseScene(scene, true); } } Debug.Log($"[StateMachine Migration] Found {foundStateMachines.Count} StateMachine(s)"); } private void PingObject(StateMachineInfo info) { if (info.isPrefab) { // Load and ping the prefab asset GameObject prefab = AssetDatabase.LoadAssetAtPath(info.assetPath); if (prefab != null) { EditorGUIUtility.PingObject(prefab); Selection.activeObject = prefab; } } else { // For scene objects, we need to open the scene if it's not already open var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); if (currentScene.path != info.assetPath) { // Scene is not currently open if (EditorUtility.DisplayDialog( "Open Scene?", $"The object is in scene:\n{info.assetPath}\n\nDo you want to open this scene?", "Yes, Open Scene", "Cancel")) { if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { EditorSceneManager.OpenScene(info.assetPath, OpenSceneMode.Single); // Find the object in the newly opened scene if (info.gameObject != null) { // The gameObject reference might be stale, find by path GameObject obj = GameObject.Find(info.gameObject.name); if (obj != null) { Selection.activeGameObject = obj; EditorGUIUtility.PingObject(obj); } } } } } else { // Scene is already open if (info.gameObject != null) { Selection.activeGameObject = info.gameObject; EditorGUIUtility.PingObject(info.gameObject); // Also scroll to it in hierarchy EditorApplication.ExecuteMenuItem("Window/General/Hierarchy"); } } } } private void MigrateAll() { int migratedCount = 0; int errorCount = 0; foreach (var info in foundStateMachines) { if (!info.isAlreadySaveable) { if (MigrateSingle(info)) { migratedCount++; } else { errorCount++; } } } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); EditorUtility.DisplayDialog( "Migration Complete", $"Migrated: {migratedCount}\nErrors: {errorCount}", "OK" ); // Refresh list to show green highlighting for newly migrated items ScanProject(); } private bool MigrateSingle(StateMachineInfo info) { try { if (info.isPrefab) { return MigratePrefab(info.assetPath); } else { return MigrateSceneObject(info.assetPath, info.gameObject); } } catch (System.Exception ex) { Debug.LogError($"[StateMachine Migration] Error migrating '{info.name}': {ex.Message}"); return false; } } private bool MigratePrefab(string prefabPath) { GameObject prefabRoot = PrefabUtility.LoadPrefabContents(prefabPath); try { var components = prefabRoot.GetComponentsInChildren(true); int migratedInPrefab = 0; foreach (var component in components) { if (component == null) continue; var componentType = component.GetType(); if (componentType.Name == "StateMachine" && componentType.Namespace == "Pixelplacement" && !(component is AppleMachine)) { if (MigrateComponent(component.gameObject, component)) { migratedInPrefab++; } } } PrefabUtility.SaveAsPrefabAsset(prefabRoot, prefabPath); Debug.Log($"[StateMachine Migration] Migrated {migratedInPrefab} component(s) in prefab: {prefabPath}"); return true; } finally { PrefabUtility.UnloadPrefabContents(prefabRoot); } } private bool MigrateSceneObject(string scenePath, GameObject gameObject) { var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); try { var components = gameObject.GetComponents(); foreach (var component in components) { if (component == null) continue; var componentType = component.GetType(); if (componentType.Name == "StateMachine" && componentType.Namespace == "Pixelplacement" && !(component is AppleMachine)) { bool success = MigrateComponent(gameObject, component); if (success) { EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); Debug.Log($"[StateMachine Migration] Migrated component in scene: {scenePath}"); } return success; } } return false; } finally { EditorSceneManager.CloseScene(scene, false); } } private bool MigrateComponent(GameObject gameObject, Component oldComponent) { // Capture old component data using SerializedObject SerializedObject oldSO = new SerializedObject(oldComponent); var defaultState = oldSO.FindProperty("defaultState"); var verbose = oldSO.FindProperty("verbose"); var allowReentry = oldSO.FindProperty("allowReentry"); var returnToDefaultOnDisable = oldSO.FindProperty("returnToDefaultOnDisable"); var onStateExited = oldSO.FindProperty("OnStateExited"); var onStateEntered = oldSO.FindProperty("OnStateEntered"); var onFirstStateEntered = oldSO.FindProperty("OnFirstStateEntered"); var onFirstStateExited = oldSO.FindProperty("OnFirstStateExited"); var onLastStateEntered = oldSO.FindProperty("OnLastStateEntered"); var onLastStateExited = oldSO.FindProperty("OnLastStateExited"); // Remove old component Object.DestroyImmediate(oldComponent); // Add new component var newSM = gameObject.AddComponent(); // Restore data using SerializedObject SerializedObject newSO = new SerializedObject(newSM); CopySerializedProperty(defaultState, newSO.FindProperty("defaultState")); CopySerializedProperty(verbose, newSO.FindProperty("verbose")); CopySerializedProperty(allowReentry, newSO.FindProperty("allowReentry")); CopySerializedProperty(returnToDefaultOnDisable, newSO.FindProperty("returnToDefaultOnDisable")); CopySerializedProperty(onStateExited, newSO.FindProperty("OnStateExited")); CopySerializedProperty(onStateEntered, newSO.FindProperty("OnStateEntered")); CopySerializedProperty(onFirstStateEntered, newSO.FindProperty("OnFirstStateEntered")); CopySerializedProperty(onFirstStateExited, newSO.FindProperty("OnFirstStateExited")); CopySerializedProperty(onLastStateEntered, newSO.FindProperty("OnLastStateEntered")); CopySerializedProperty(onLastStateExited, newSO.FindProperty("OnLastStateExited")); // Set a custom Save ID based on GameObject name (simple and readable) // Users can customize this in the inspector if needed var customSaveIdProperty = newSO.FindProperty("customSaveId"); if (customSaveIdProperty != null) { // Use GameObject name as the custom ID (scene name will be added automatically by GetSaveId) customSaveIdProperty.stringValue = gameObject.name; Debug.Log($"[Migration] Set custom Save ID: '{customSaveIdProperty.stringValue}' for '{gameObject.name}'"); } newSO.ApplyModifiedProperties(); EditorUtility.SetDirty(gameObject); return true; } private void CopySerializedProperty(SerializedProperty source, SerializedProperty dest) { if (source == null || dest == null) return; switch (source.propertyType) { case SerializedPropertyType.Integer: dest.intValue = source.intValue; break; case SerializedPropertyType.Boolean: dest.boolValue = source.boolValue; break; case SerializedPropertyType.Float: dest.floatValue = source.floatValue; break; case SerializedPropertyType.String: dest.stringValue = source.stringValue; break; case SerializedPropertyType.Color: dest.colorValue = source.colorValue; break; case SerializedPropertyType.ObjectReference: dest.objectReferenceValue = source.objectReferenceValue; break; case SerializedPropertyType.LayerMask: dest.intValue = source.intValue; break; case SerializedPropertyType.Enum: dest.enumValueIndex = source.enumValueIndex; break; case SerializedPropertyType.Vector2: dest.vector2Value = source.vector2Value; break; case SerializedPropertyType.Vector3: dest.vector3Value = source.vector3Value; break; case SerializedPropertyType.Vector4: dest.vector4Value = source.vector4Value; break; case SerializedPropertyType.Rect: dest.rectValue = source.rectValue; break; case SerializedPropertyType.ArraySize: dest.arraySize = source.arraySize; break; case SerializedPropertyType.Character: dest.intValue = source.intValue; break; case SerializedPropertyType.AnimationCurve: dest.animationCurveValue = source.animationCurveValue; break; case SerializedPropertyType.Bounds: dest.boundsValue = source.boundsValue; break; case SerializedPropertyType.Quaternion: dest.quaternionValue = source.quaternionValue; break; case SerializedPropertyType.Generic: // Handle UnityEvent and other generic types by copying children CopyGenericProperty(source, dest); break; } } private void CopyGenericProperty(SerializedProperty source, SerializedProperty dest) { // For arrays and lists if (source.isArray) { dest.arraySize = source.arraySize; for (int i = 0; i < source.arraySize; i++) { CopySerializedProperty(source.GetArrayElementAtIndex(i), dest.GetArrayElementAtIndex(i)); } } else { // For complex objects, copy all children SerializedProperty sourceIterator = source.Copy(); SerializedProperty destIterator = dest.Copy(); bool enterChildren = true; while (sourceIterator.Next(enterChildren)) { // Only process immediate children if (!sourceIterator.propertyPath.StartsWith(source.propertyPath + ".")) break; enterChildren = false; // Find corresponding property in destination string relativePath = sourceIterator.propertyPath.Substring(source.propertyPath.Length + 1); SerializedProperty destChild = dest.FindPropertyRelative(relativePath); if (destChild != null) { CopySerializedProperty(sourceIterator, destChild); } } } } private class StateMachineInfo { public string name; public string path; public bool isPrefab; public bool isAlreadySaveable; public string assetPath; public GameObject gameObject; } } public static class TransformExtensions { public static string GetHierarchyPath(this Transform transform) { string path = transform.name; while (transform.parent != null) { transform = transform.parent; path = transform.name + "/" + path; } return path; } } }