Move some tools for consistency, audo add feature for card debugger

This commit is contained in:
Michal Pikulski
2025-11-07 09:32:43 +01:00
parent 3e607f3857
commit 0d8702a5f6
12 changed files with 73 additions and 11 deletions

View File

@@ -0,0 +1,649 @@
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
using System;
using System.Collections.Generic;
using System.IO;
using Interactions;
namespace Editor
{
public class RemoveInteractableBaseComponents : EditorWindow
{
private List<string> problematicPrefabs = new List<string>();
private List<string> problematicScenes = new List<string>();
private Vector2 scrollPosition;
private bool hasScanned;
private int componentsFound;
[MenuItem("AppleHills/Developer/Remove InteractableBase Components")]
public static void ShowWindow()
{
var window = GetWindow<RemoveInteractableBaseComponents>("Remove InteractableBase");
window.minSize = new Vector2(700, 500);
}
private void OnGUI()
{
GUILayout.Label("Remove InteractableBase Component References", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"This tool finds and removes EXACT InteractableBase components from prefabs and scenes.\n\n" +
"Only finds the bare base class, NOT derived types like Pickup/ItemSlot/OneClickInteraction.\n\n" +
"If components depend on InteractableBase, you'll be prompted to replace it.",
MessageType.Info);
EditorGUILayout.Space();
if (GUILayout.Button("Scan All Prefabs and Scenes", GUILayout.Height(35)))
{
ScanAll();
}
EditorGUILayout.Space();
if (hasScanned)
{
EditorGUILayout.LabelField($"Found {componentsFound} exact InteractableBase components", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"In {problematicPrefabs.Count} prefabs");
EditorGUILayout.LabelField($"In {problematicScenes.Count} scenes");
if (componentsFound > 0)
{
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
if (problematicPrefabs.Count > 0 && GUILayout.Button($"Remove from Prefabs ({problematicPrefabs.Count})", GUILayout.Height(35)))
{
RemoveFromAllPrefabs();
}
if (problematicScenes.Count > 0 && GUILayout.Button($"Remove from Scenes ({problematicScenes.Count})", GUILayout.Height(35)))
{
RemoveFromAllScenes();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
if (GUILayout.Button("Remove All (Prefabs + Scenes)", GUILayout.Height(35)))
{
RemoveAll();
}
EditorGUILayout.Space();
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
if (problematicPrefabs.Count > 0)
{
EditorGUILayout.LabelField("Prefabs:", EditorStyles.boldLabel);
foreach (var prefabPath in problematicPrefabs)
{
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField(prefabPath);
if (GUILayout.Button("Remove", GUILayout.Width(80)))
{
RemoveFromPrefab(prefabPath);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space();
}
if (problematicScenes.Count > 0)
{
EditorGUILayout.LabelField("Scenes:", EditorStyles.boldLabel);
foreach (var scenePath in problematicScenes)
{
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField(scenePath);
if (GUILayout.Button("Remove", GUILayout.Width(80)))
{
RemoveFromScene(scenePath);
}
EditorGUILayout.EndHorizontal();
}
}
EditorGUILayout.EndScrollView();
}
else
{
EditorGUILayout.HelpBox("No exact InteractableBase components found! All clean.", MessageType.Info);
}
}
}
private void ScanAll()
{
problematicPrefabs.Clear();
problematicScenes.Clear();
componentsFound = 0;
hasScanned = true;
ScanPrefabs();
ScanScenes();
Debug.Log($"<color=cyan>[Scan Complete]</color> Found {componentsFound} exact InteractableBase components in {problematicPrefabs.Count} prefabs and {problematicScenes.Count} scenes.");
}
private void ScanPrefabs()
{
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
EditorUtility.DisplayProgressBar("Scanning Prefabs", "Starting...", 0f);
for (int i = 0; i < prefabGuids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
EditorUtility.DisplayProgressBar("Scanning Prefabs",
$"Checking {i + 1}/{prefabGuids.Length}: {Path.GetFileName(path)}",
(float)i / prefabGuids.Length);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab != null)
{
// Check if this prefab or any of its children have EXACTLY InteractableBase (not derived types)
InteractableBase[] components = prefab.GetComponentsInChildren<InteractableBase>(true);
int exactMatches = 0;
foreach (var component in components)
{
if (component != null && component.GetType() == typeof(InteractableBase))
{
exactMatches++;
}
}
if (exactMatches > 0)
{
problematicPrefabs.Add(path);
componentsFound += exactMatches;
}
}
}
EditorUtility.ClearProgressBar();
}
private void ScanScenes()
{
string[] sceneGuids = AssetDatabase.FindAssets("t:Scene", new[] { "Assets/Scenes" });
EditorUtility.DisplayProgressBar("Scanning Scenes", "Starting...", 0f);
string currentScenePath = SceneManager.GetActiveScene().path;
for (int i = 0; i < sceneGuids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(sceneGuids[i]);
EditorUtility.DisplayProgressBar("Scanning Scenes",
$"Checking {i + 1}/{sceneGuids.Length}: {Path.GetFileName(path)}",
(float)i / sceneGuids.Length);
EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
// Find all InteractableBase components in the scene
InteractableBase[] components = GameObject.FindObjectsByType<InteractableBase>(FindObjectsSortMode.None);
int exactMatches = 0;
foreach (var component in components)
{
if (component != null && component.GetType() == typeof(InteractableBase))
{
exactMatches++;
}
}
if (exactMatches > 0)
{
problematicScenes.Add(path);
componentsFound += exactMatches;
}
}
// Restore original scene
if (!string.IsNullOrEmpty(currentScenePath))
{
EditorSceneManager.OpenScene(currentScenePath);
}
EditorUtility.ClearProgressBar();
}
private void RemoveFromAllPrefabs()
{
if (!EditorUtility.DisplayDialog("Confirm Removal",
$"This will remove InteractableBase components from {problematicPrefabs.Count} prefabs.\n\n" +
"This cannot be undone (unless you use version control).\n\nContinue?",
"Yes, Remove", "Cancel"))
{
return;
}
int removedCount = 0;
for (int i = 0; i < problematicPrefabs.Count; i++)
{
string path = problematicPrefabs[i];
EditorUtility.DisplayProgressBar("Removing Components from Prefabs",
$"Processing {i + 1}/{problematicPrefabs.Count}: {Path.GetFileName(path)}",
(float)i / problematicPrefabs.Count);
removedCount += RemoveFromPrefab(path);
}
EditorUtility.ClearProgressBar();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"<color=green>[Prefab Cleanup Complete]</color> Removed {removedCount} InteractableBase components from prefabs.");
ScanAll();
}
private void RemoveFromAllScenes()
{
if (!EditorUtility.DisplayDialog("Confirm Removal",
$"This will remove InteractableBase components from {problematicScenes.Count} scenes.\n\n" +
"This cannot be undone (unless you use version control).\n\nContinue?",
"Yes, Remove", "Cancel"))
{
return;
}
int removedCount = 0;
string currentScenePath = SceneManager.GetActiveScene().path;
for (int i = 0; i < problematicScenes.Count; i++)
{
string path = problematicScenes[i];
EditorUtility.DisplayProgressBar("Removing Components from Scenes",
$"Processing {i + 1}/{problematicScenes.Count}: {Path.GetFileName(path)}",
(float)i / problematicScenes.Count);
removedCount += RemoveFromScene(path);
}
// Restore original scene
if (!string.IsNullOrEmpty(currentScenePath))
{
EditorSceneManager.OpenScene(currentScenePath);
}
EditorUtility.ClearProgressBar();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"<color=green>[Scene Cleanup Complete]</color> Removed {removedCount} InteractableBase components from scenes.");
ScanAll();
}
private void RemoveAll()
{
if (!EditorUtility.DisplayDialog("Confirm Removal",
$"This will remove {componentsFound} InteractableBase components from:\n" +
$"• {problematicPrefabs.Count} prefabs\n" +
$"• {problematicScenes.Count} scenes\n\n" +
"This cannot be undone (unless you use version control).\n\nContinue?",
"Yes, Remove", "Cancel"))
{
return;
}
int removedCount = 0;
// Remove from prefabs
for (int i = 0; i < problematicPrefabs.Count; i++)
{
string path = problematicPrefabs[i];
EditorUtility.DisplayProgressBar("Removing Components",
$"Prefabs {i + 1}/{problematicPrefabs.Count}: {Path.GetFileName(path)}",
(float)i / (problematicPrefabs.Count + problematicScenes.Count));
removedCount += RemoveFromPrefab(path);
}
// Remove from scenes
string currentScenePath = SceneManager.GetActiveScene().path;
for (int i = 0; i < problematicScenes.Count; i++)
{
string path = problematicScenes[i];
EditorUtility.DisplayProgressBar("Removing Components",
$"Scenes {i + 1}/{problematicScenes.Count}: {Path.GetFileName(path)}",
(float)(problematicPrefabs.Count + i) / (problematicPrefabs.Count + problematicScenes.Count));
removedCount += RemoveFromScene(path);
}
// Restore original scene
if (!string.IsNullOrEmpty(currentScenePath))
{
EditorSceneManager.OpenScene(currentScenePath);
}
EditorUtility.ClearProgressBar();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"<color=green>[Removal Complete]</color> Removed {removedCount} InteractableBase components.");
ScanAll();
}
private int RemoveFromPrefab(string assetPath)
{
int removed = 0;
try
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
if (prefab == null)
{
Debug.LogWarning($"Could not load prefab at path: {assetPath}");
return 0;
}
string prefabPath = AssetDatabase.GetAssetPath(prefab);
GameObject prefabContents = null;
try
{
prefabContents = PrefabUtility.LoadPrefabContents(prefabPath);
}
catch (Exception loadEx)
{
Debug.LogError($"Failed to load prefab contents for {assetPath}: {loadEx.Message}");
return 0;
}
if (prefabContents == null)
{
Debug.LogWarning($"Prefab contents are null for: {assetPath}");
return 0;
}
InteractableBase[] components = prefabContents.GetComponentsInChildren<InteractableBase>(true);
if (components == null || components.Length == 0)
{
PrefabUtility.UnloadPrefabContents(prefabContents);
return 0;
}
foreach (var component in components)
{
if (component == null)
continue;
// Check if it's EXACTLY InteractableBase (not a derived type)
if (component.GetType() == typeof(InteractableBase))
{
// Cache references before destroying
GameObject targetObject = component.gameObject;
string objectName = targetObject != null ? targetObject.name : "Unknown";
// Check if GameObject already has a derived InteractableBase type
bool hasPickup = targetObject.GetComponent<Pickup>() != null;
bool hasItemSlot = targetObject.GetComponent<ItemSlot>() != null;
bool hasOneClick = targetObject.GetComponent<OneClickInteraction>() != null;
if (hasPickup || hasItemSlot || hasOneClick)
{
// GameObject already has a concrete type, safe to remove bare base class
DestroyImmediate(component);
removed++;
string existingType = hasItemSlot ? "ItemSlot" : (hasPickup ? "Pickup" : "OneClickInteraction");
Debug.Log($"<color=green>[Removed]</color> Bare InteractableBase from '{objectName}' (already has {existingType}) in prefab '{Path.GetFileName(assetPath)}'");
continue;
}
// Check what other components depend on InteractableBase
Component[] allComponents = targetObject.GetComponents<Component>();
List<string> dependentComponents = new List<string>();
foreach (var otherComponent in allComponents)
{
if (otherComponent == null || otherComponent == component)
continue;
var requireAttributes = otherComponent.GetType().GetCustomAttributes(typeof(RequireComponent), true);
foreach (RequireComponent attr in requireAttributes)
{
if (attr.m_Type0 == typeof(InteractableBase) ||
attr.m_Type1 == typeof(InteractableBase) ||
attr.m_Type2 == typeof(InteractableBase))
{
dependentComponents.Add(otherComponent.GetType().Name);
}
}
}
if (dependentComponents.Count > 0)
{
string dependencyList = string.Join(", ", dependentComponents);
string message = $"GameObject '{objectName}' in prefab '{Path.GetFileName(assetPath)}' has InteractableBase, " +
$"but these components depend on it:\n\n{dependencyList}\n\n" +
"Replace InteractableBase with:";
int choice = EditorUtility.DisplayDialogComplex(
"Component Dependency Detected",
message,
"Pickup",
"ItemSlot",
"OneClickInteraction");
Type replacementType = choice switch
{
0 => typeof(Pickup),
1 => typeof(ItemSlot),
2 => typeof(OneClickInteraction),
_ => null
};
if (replacementType != null)
{
// Cache component data before destroying
bool isOneTime = component.isOneTime;
float cooldown = component.cooldown;
CharacterToInteract characterToInteract = component.characterToInteract;
DestroyImmediate(component);
var newComponent = targetObject.AddComponent(replacementType) as InteractableBase;
if (newComponent != null)
{
newComponent.isOneTime = isOneTime;
newComponent.cooldown = cooldown;
newComponent.characterToInteract = characterToInteract;
removed++;
Debug.Log($"<color=cyan>[Replaced]</color> InteractableBase with {replacementType.Name} on '{objectName}' in prefab '{Path.GetFileName(assetPath)}'");
}
}
else
{
Debug.LogWarning($"Skipped removing InteractableBase from '{objectName}' - no replacement chosen");
}
}
else
{
DestroyImmediate(component);
removed++;
Debug.Log($"<color=yellow>[Removed]</color> InteractableBase from '{objectName}' in prefab '{Path.GetFileName(assetPath)}'");
}
}
}
if (removed > 0)
{
try
{
PrefabUtility.SaveAsPrefabAsset(prefabContents, prefabPath);
}
catch (Exception saveEx)
{
Debug.LogError($"Failed to save prefab {assetPath}: {saveEx.Message}");
}
}
PrefabUtility.UnloadPrefabContents(prefabContents);
}
catch (Exception ex)
{
Debug.LogError($"Error removing components from prefab {assetPath}: {ex.Message}\nStack: {ex.StackTrace}");
}
return removed;
}
private int RemoveFromScene(string scenePath)
{
int removed = 0;
try
{
Scene scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
if (!scene.isLoaded)
{
Debug.LogWarning($"Scene not loaded: {scenePath}");
return 0;
}
InteractableBase[] components = GameObject.FindObjectsByType<InteractableBase>(FindObjectsSortMode.None);
if (components == null || components.Length == 0)
{
return 0;
}
foreach (var component in components)
{
if (component == null)
continue;
if (component.GetType() == typeof(InteractableBase))
{
// Cache references before destroying
GameObject targetObject = component.gameObject;
string objectName = targetObject != null ? targetObject.name : "Unknown";
// Check if GameObject already has a derived InteractableBase type
bool hasPickup = targetObject.GetComponent<Pickup>() != null;
bool hasItemSlot = targetObject.GetComponent<ItemSlot>() != null;
bool hasOneClick = targetObject.GetComponent<OneClickInteraction>() != null;
if (hasPickup || hasItemSlot || hasOneClick)
{
// GameObject already has a concrete type, safe to remove bare base class
DestroyImmediate(component);
removed++;
string existingType = hasItemSlot ? "ItemSlot" : (hasPickup ? "Pickup" : "OneClickInteraction");
Debug.Log($"<color=green>[Removed]</color> Bare InteractableBase from '{objectName}' (already has {existingType}) in scene '{Path.GetFileName(scenePath)}'");
continue;
}
Component[] allComponents = targetObject.GetComponents<Component>();
List<string> dependentComponents = new List<string>();
foreach (var otherComponent in allComponents)
{
if (otherComponent == null || otherComponent == component)
continue;
var requireAttributes = otherComponent.GetType().GetCustomAttributes(typeof(RequireComponent), true);
foreach (RequireComponent attr in requireAttributes)
{
if (attr.m_Type0 == typeof(InteractableBase) ||
attr.m_Type1 == typeof(InteractableBase) ||
attr.m_Type2 == typeof(InteractableBase))
{
dependentComponents.Add(otherComponent.GetType().Name);
}
}
}
if (dependentComponents.Count > 0)
{
string dependencyList = string.Join(", ", dependentComponents);
string message = $"GameObject '{objectName}' in scene '{Path.GetFileName(scenePath)}' has InteractableBase, " +
$"but these components depend on it:\n\n{dependencyList}\n\n" +
"Replace InteractableBase with:";
int choice = EditorUtility.DisplayDialogComplex(
"Component Dependency Detected",
message,
"Pickup",
"ItemSlot",
"OneClickInteraction");
Type replacementType = choice switch
{
0 => typeof(Pickup),
1 => typeof(ItemSlot),
2 => typeof(OneClickInteraction),
_ => null
};
if (replacementType != null)
{
// Cache component data before destroying
bool isOneTime = component.isOneTime;
float cooldown = component.cooldown;
CharacterToInteract characterToInteract = component.characterToInteract;
DestroyImmediate(component);
var newComponent = targetObject.AddComponent(replacementType) as InteractableBase;
if (newComponent != null)
{
newComponent.isOneTime = isOneTime;
newComponent.cooldown = cooldown;
newComponent.characterToInteract = characterToInteract;
removed++;
Debug.Log($"<color=cyan>[Replaced]</color> InteractableBase with {replacementType.Name} on '{objectName}' in scene '{Path.GetFileName(scenePath)}'");
}
}
else
{
Debug.LogWarning($"Skipped removing InteractableBase from '{objectName}' - no replacement chosen");
}
}
else
{
DestroyImmediate(component);
removed++;
Debug.Log($"<color=yellow>[Removed]</color> InteractableBase from '{objectName}' in scene '{Path.GetFileName(scenePath)}'");
}
}
}
if (removed > 0)
{
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
}
}
catch (Exception ex)
{
Debug.LogError($"Error removing components from scene {scenePath}: {ex.Message}\nStack: {ex.StackTrace}");
}
return removed;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: da895c0622e34ef8a18675993eec9877
timeCreated: 1762024152

View File

@@ -0,0 +1,228 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace Editor
{
public class RemoveOldInteractableReferences : EditorWindow
{
private List<string> problematicPrefabs = new List<string>();
private Vector2 scrollPosition;
private bool hasScanned = false;
[MenuItem("AppleHills/Developer/Remove Old Interactable References")]
public static void ShowWindow()
{
var window = GetWindow<RemoveOldInteractableReferences>("Clean Old Interactables");
window.minSize = new Vector2(600, 400);
}
private void OnGUI()
{
GUILayout.Label("Remove Old Interactable/InteractableBase References", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"This tool finds and removes references to:\n" +
"- Interactable (old script name)\n" +
"- InteractableBase (abstract class - not allowed on prefabs)\n\n" +
"These should be replaced by concrete types: Pickup, ItemSlot, or OneClickInteraction",
MessageType.Info);
EditorGUILayout.Space();
if (GUILayout.Button("Scan All Prefabs", GUILayout.Height(30)))
{
ScanPrefabs();
}
EditorGUILayout.Space();
if (hasScanned)
{
EditorGUILayout.LabelField($"Found {problematicPrefabs.Count} prefabs with old references", EditorStyles.boldLabel);
if (problematicPrefabs.Count > 0)
{
EditorGUILayout.Space();
if (GUILayout.Button("Clean All Prefabs", GUILayout.Height(30)))
{
CleanAllPrefabs();
}
EditorGUILayout.Space();
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
foreach (var prefabPath in problematicPrefabs)
{
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField(prefabPath);
if (GUILayout.Button("Clean This", GUILayout.Width(80)))
{
CleanSinglePrefab(prefabPath);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
else
{
EditorGUILayout.HelpBox("No problematic prefabs found! All clean.", MessageType.Info);
}
}
}
private void ScanPrefabs()
{
problematicPrefabs.Clear();
hasScanned = true;
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
EditorUtility.DisplayProgressBar("Scanning Prefabs", "Starting...", 0f);
for (int i = 0; i < prefabGuids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
EditorUtility.DisplayProgressBar("Scanning Prefabs",
$"Checking {i + 1}/{prefabGuids.Length}: {Path.GetFileName(path)}",
(float)i / prefabGuids.Length);
if (PrefabHasOldInteractableReference(path))
{
problematicPrefabs.Add(path);
}
}
EditorUtility.ClearProgressBar();
Debug.Log($"<color=cyan>[Scan Complete]</color> Found {problematicPrefabs.Count} prefabs with old Interactable/InteractableBase references.");
}
private bool PrefabHasOldInteractableReference(string assetPath)
{
try
{
string fullPath = Path.GetFullPath(assetPath);
string content = File.ReadAllText(fullPath);
// Look for GUID of Interactable script (11500000 is MonoBehaviour type)
// We're looking for the script reference pattern in YAML
// Pattern: m_Script: {fileID: 11500000, guid: SCRIPT_GUID, type: 3}
// Check if content contains "Interactable" class name references
// This is a simple text search - if the YAML contains these class names, it likely references them
if (content.Contains("InteractableBase") ||
(content.Contains("Interactable") && !content.Contains("OneClickInteraction")))
{
// Additional check: Look for MonoBehaviour blocks with missing scripts (fileID: 0)
if (Regex.IsMatch(content, @"m_Script:\s*\{fileID:\s*0\}"))
{
return true;
}
// Check for direct class name matches in script references
if (Regex.IsMatch(content, @"m_Name:\s*(Interactable|InteractableBase)"))
{
return true;
}
}
return false;
}
catch (System.Exception ex)
{
Debug.LogWarning($"Error scanning {assetPath}: {ex.Message}");
return false;
}
}
private void CleanAllPrefabs()
{
if (!EditorUtility.DisplayDialog("Confirm Cleanup",
$"This will remove old Interactable/InteractableBase references from {problematicPrefabs.Count} prefabs.\n\nThis cannot be undone (unless you use version control).\n\nContinue?",
"Yes, Clean", "Cancel"))
{
return;
}
int cleanedCount = 0;
for (int i = 0; i < problematicPrefabs.Count; i++)
{
string path = problematicPrefabs[i];
EditorUtility.DisplayProgressBar("Cleaning Prefabs",
$"Cleaning {i + 1}/{problematicPrefabs.Count}: {Path.GetFileName(path)}",
(float)i / problematicPrefabs.Count);
if (CleanPrefabFile(path))
{
cleanedCount++;
}
}
EditorUtility.ClearProgressBar();
AssetDatabase.Refresh();
Debug.Log($"<color=green>[Cleanup Complete]</color> Cleaned {cleanedCount} prefabs.");
// Re-scan to update the list
ScanPrefabs();
}
private void CleanSinglePrefab(string assetPath)
{
if (CleanPrefabFile(assetPath))
{
Debug.Log($"<color=green>[Cleaned]</color> {assetPath}");
AssetDatabase.Refresh();
// Re-scan to update the list
ScanPrefabs();
}
}
private bool CleanPrefabFile(string assetPath)
{
try
{
string fullPath = Path.GetFullPath(assetPath);
string content = File.ReadAllText(fullPath);
string originalContent = content;
// Pattern 1: Remove entire MonoBehaviour component blocks with missing scripts (fileID: 0)
// This removes the component header and all its properties until the next component or end
string missingScriptPattern = @"--- !u!114 &\d+\r?\nMonoBehaviour:(?:\r?\n(?!---).+)*?\r?\n m_Script: \{fileID: 0\}(?:\r?\n(?!---).+)*";
content = Regex.Replace(content, missingScriptPattern, "", RegexOptions.Multiline);
// Pattern 2: Remove MonoBehaviour blocks that explicitly reference InteractableBase or Interactable
// This is more aggressive and targets the class name directly
string interactablePattern = @"--- !u!114 &\d+\r?\nMonoBehaviour:(?:\r?\n(?!---).+)*?\r?\n m_Name: (?:Interactable|InteractableBase)(?:\r?\n(?!---).+)*";
content = Regex.Replace(content, interactablePattern, "", RegexOptions.Multiline);
if (content != originalContent)
{
// Clean up any double blank lines that might have been created
content = Regex.Replace(content, @"(\r?\n){3,}", "\n\n");
File.WriteAllText(fullPath, content);
return true;
}
return false;
}
catch (System.Exception ex)
{
Debug.LogError($"Error cleaning {assetPath}: {ex.Message}");
return false;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d898bc44012542c0942b632b56cea3dc
timeCreated: 1762023714

View File

@@ -0,0 +1,575 @@
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using Core.SaveLoad;
using System.Collections.Generic;
using System.Linq;
namespace Editor
{
/// <summary>
/// Editor utility to migrate StateMachine components to SaveableStateMachine.
/// </summary>
public class StateMachineMigrationTool : EditorWindow
{
private Vector2 scrollPosition;
private List<StateMachineInfo> foundStateMachines = new List<StateMachineInfo>();
private bool showPrefabs = true;
private bool showScenes = true;
[MenuItem("AppleHills/Developer/Migrate StateMachines to Saveable")]
public static void ShowWindow()
{
var window = GetWindow<StateMachineMigrationTool>("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<GameObject>(path);
if (prefab != null)
{
// Use GetComponents to find Pixelplacement.StateMachine
var components = prefab.GetComponentsInChildren<Component>(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<Component>(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<GameObject>(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<Component>(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<Component>();
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<AppleMachine>();
// 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;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 13897a2afcff4d21a3eb20fe5092bf7a
timeCreated: 1762121174