using System; using System.IO; using System.Collections.Generic; using UnityEngine; using UnityEditor; using UnityEditor.Callbacks; using Bonsai.Utility; namespace UNEB { /// /// Handles the saving and loading of tree assets. /// public class SaveManager { public enum SaveState { NoGraph, TempGraph, SavedGraph }; // The FSM used to structure the logic control of saving and loading. private StateMachine _saveFSM; // The events that dictate the flow of the manager's FSM. private enum SaveOp { None, New, Load, Save, SaveAs }; private SaveOp _saveOp = SaveOp.None; private NodeEditorWindow _window; private const string kRootUNEB = "UNEB"; // Path that stores temporary graphs. private const string kTempGraphDirectory = "TempGraphsUNEB"; private const string kTempFileName = "TempNodeGraphUNEB"; public SaveManager(NodeEditorWindow w) { _window = w; _saveFSM = new StateMachine(); var noGraph = new StateMachine.State(SaveState.NoGraph); var tempGraph = new StateMachine.State(SaveState.TempGraph); var savedGraph = new StateMachine.State(SaveState.SavedGraph); _saveFSM.AddState(noGraph); _saveFSM.AddState(tempGraph); _saveFSM.AddState(savedGraph); // Actions to take when starting out on a window with no graph. _saveFSM.AddTransition(noGraph, tempGraph, isNewRequested, createNewOnto_Window_WithTempOrEmpty); _saveFSM.AddTransition(noGraph, savedGraph, isLoadRequested, loadOnto_EmptyWindow); // Actions to take when the window has a temp graph. _saveFSM.AddTransition(tempGraph, tempGraph, isNewRequested, createNewOnto_Window_WithTempOrEmpty); _saveFSM.AddTransition(tempGraph, savedGraph, isSaveOrSaveAsRequested, saveTempAs); _saveFSM.AddTransition(tempGraph, savedGraph, isLoadRequested, loadOnto_Window_WithTempgraph); // Actions to take when the window has a valid graph (already saved). _saveFSM.AddTransition(savedGraph, savedGraph, isSaveRequested, save); _saveFSM.AddTransition(savedGraph, savedGraph, isSaveAsRequested, saveCloneAs); _saveFSM.AddTransition(savedGraph, savedGraph, isLoadRequested, loadOnto_Window_WithSavedgraph); _saveFSM.AddTransition(savedGraph, tempGraph, isNewRequested, createNewOnto_Window_WithSavedgraph); // Consume the save operation even after the transition is made. _saveFSM.OnStateChangedEvent += () => { _saveOp = SaveOp.None; }; InitState(); NodeConnection.OnConnectionCreated -= saveConnection; NodeConnection.OnConnectionCreated += saveConnection; } /// /// This hanldes setting up the proper state based on the window's graph. /// internal void InitState() { // If the window has a valid graph and editable. if (_window.graph != null && _window.GetMode() == NodeEditorWindow.Mode.Edit) { string path = getCurrentGraphPath(); // If the graph is temp. if (path.Contains(kTempGraphDirectory)) { SetState(SaveState.TempGraph); } // If the graph is saved (not a temp). else { SetState(SaveState.SavedGraph); } } // Window is fresh, no graph yet set. else { SetState(SaveState.NoGraph); } } /// /// Get the path from open file dialog. /// /// private string getGraphFilePath() { string path = EditorUtility.OpenFilePanel("Open Node Graph", "Assets/", "asset"); // If the path is outside the project's asset folder. if (!path.Contains(Application.dataPath)) { // If the selection was not cancelled... if (!string.IsNullOrEmpty(path)) { _window.ShowNotification(new GUIContent("Please select a Graph asset within the project's Asset folder.")); return null; } } return path; } /// /// Assumes that the path is already valid. /// /// private void loadGraph(string path) { int assetIndex = path.IndexOf("/Assets/"); path = path.Substring(assetIndex + 1); var graph = AssetDatabase.LoadAssetAtPath(path); _window.SetGraph(graph); } /// /// Gets the file path to save the canavs at. /// /// private string getSaveFilePath() { string path = EditorUtility.SaveFilePanelInProject("Save Node Graph", "NewNodeGraph", "asset", "Select a destination to save the graph."); if (string.IsNullOrEmpty(path)) { return ""; } return path; } #region Save Operations /// /// Creates and adds a node to the graph. /// /// /// /// public static Node CreateNode(Type t, NodeGraph g) { try { var node = ScriptableObject.CreateInstance(t) as Node; AssetDatabase.AddObjectToAsset(node, g); // Optional, set reference to graph: node.graph = g node.Init(); g.Add(node); return node; } catch (Exception e) { throw new UnityException(e.Message); } } /// /// Creates and adds a node to the graph. /// /// /// /// public static Node CreateNode(NodeGraph g) where T : Node { var node = ScriptableObject.CreateInstance(); AssetDatabase.AddObjectToAsset(node, g); // Optional, set reference to graph: node.graph = g node.Init(); g.Add(node); return node; } /// /// Creates a graph asset and saves it. /// /// The full path including name and extension. /// public static NodeGraph CreateNodeGraph(string path) { // We create a graph asset in the data base in order to add node assets // under the graph. This way things are organized in the editor. // // The drawback is that we need to create a temp asset for the tree // and make sure it does not linger if the temp asset is discarded. // // This means that we need to have a persistent directoy to store temp // assets. var graph = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(graph, path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return graph; } /// /// Creates a new temporary node graph. /// /// private NodeGraph createNew() { string tempPath = getTempFilePath(); if (!string.IsNullOrEmpty(tempPath)) { _window.ShowNotification(new GUIContent("New Graph Created")); return CreateNodeGraph(tempPath); } return null; } // Create a new temp graph on an empty window or with a temp graph. private bool createNewOnto_Window_WithTempOrEmpty() { _window.SetGraph(createNew()); return true; } // Saves the current active graph then loads a new graph. private bool createNewOnto_Window_WithSavedgraph() { // Save the old graph to avoid loss. AssetDatabase.SaveAssets(); _window.SetGraph(createNew()); return true; } // Load a graph to a window that has no graph active. private bool loadOnto_EmptyWindow() { loadGraph(getGraphFilePath()); return true; } // Load a graph to a window that has a temp graph active. private bool loadOnto_Window_WithTempgraph() { string path = getGraphFilePath(); if (!string.IsNullOrEmpty(path)) { // Get rid of the temporary graph. AssetDatabase.DeleteAsset(getCurrentGraphPath()); loadGraph(path); return true; } return false; } // Load a graph to a window that has a saved graph active. private bool loadOnto_Window_WithSavedgraph() { string path = getGraphFilePath(); if (!string.IsNullOrEmpty(path)) { // Save the old graph. save(); loadGraph(path); return true; } return false; } // Makes the temporary graph into a saved graph. private bool saveTempAs() { string newPath = getSaveFilePath(); string currentPath = getCurrentGraphPath(); //If asset exists on path, delete it first. if (AssetDatabase.LoadAssetAtPath(newPath) != null) { AssetDatabase.DeleteAsset(newPath); } string result = AssetDatabase.ValidateMoveAsset(currentPath, newPath); if (result.Length == 0) { AssetDatabase.MoveAsset(currentPath, newPath); save(); return true; } else { Debug.LogError(result); return false; } } // Copies the current active graph to a new location. private bool saveCloneAs() { string newPath = getSaveFilePath(); if (!string.IsNullOrEmpty(newPath)) { string currentPath = getCurrentGraphPath(); AssetDatabase.CopyAsset(currentPath, newPath); AssetDatabase.SetMainObject(_window.graph, currentPath); save(); return true; } return false; } // Saves the current graph (not a temp graph). private bool save() { _window.graph.OnSave(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); _window.ShowNotification(new GUIContent("Graph Saved")); return true; } // Helper method for the NodeConnection.OnConnectionCreated callback. private void saveConnection(NodeConnection conn) { if (!AssetDatabase.Contains(conn)) { AssetDatabase.AddObjectToAsset(conn, _window.graph); } } #endregion /// /// Handles deleting temporary graph or saving valid graph. /// internal void Cleanup() { // Only save/delete things if we are in edit mode. if (_window.GetMode() != NodeEditorWindow.Mode.Edit) { return; } SaveState state = _saveFSM.CurrentState.Value; if (state == SaveState.TempGraph) { AssetDatabase.DeleteAsset(getCurrentGraphPath()); } else if (state == SaveState.SavedGraph) { save(); } } /* * These are conditions used the save FSM to know when to transition. * */ private bool isNewRequested() { return _saveOp == SaveOp.New; } private bool isLoadRequested() { return _saveOp == SaveOp.Load; } private bool isSaveRequested() { return _saveOp == SaveOp.Save; } private bool isSaveAsRequested() { return _saveOp == SaveOp.SaveAs; } private bool isSaveOrSaveAsRequested() { return isSaveAsRequested() || isSaveRequested(); } /* * These are the events that drive the save manager. * Whenever one of this is fired, the save operation is set * and the save FSM updated. * */ internal void RequestNew() { _saveOp = SaveOp.New; _saveFSM.Update(); } internal void RequestLoad() { _saveOp = SaveOp.Load; _saveFSM.Update(); } internal void RequestSave() { _saveOp = SaveOp.Save; _saveFSM.Update(); } internal void RequestSaveAs() { _saveOp = SaveOp.SaveAs; _saveFSM.Update(); } private string getTempFilePath() { string tempRoot = getTempDirPath(); if (string.IsNullOrEmpty(tempRoot)) { return ""; } string filename = kTempFileName + _window.GetInstanceID().ToString().Ext("asset"); return tempRoot.Dir(filename); } internal void SetState(SaveState state) { _saveFSM.SetCurrentState(state); } internal bool IsInNographState() { return _saveFSM.CurrentState.Value == SaveState.NoGraph; } internal SaveState CurrentState() { return _saveFSM.CurrentState.Value; } private string getCurrentGraphPath() { return AssetDatabase.GetAssetPath(_window.graph); } private string getTempDirPath() { string[] dirs = Directory.GetDirectories(Application.dataPath, kTempGraphDirectory, SearchOption.AllDirectories); // Return first occurance containing targetFolderName. if (dirs.Length != 0) { return getTempPathRelativeToAssets(dirs[0]); } // Could not find anything. Make the folder string rootPath = getPathToRootUNEB(); if (!string.IsNullOrEmpty(rootPath)) { var dirInfo = Directory.CreateDirectory(rootPath.Dir(kTempGraphDirectory)); return getTempPathRelativeToAssets(dirInfo.FullName); } else { return ""; } } private static string getPathToRootUNEB() { // Find the UNEB project root directory within the Unity project. var dirs = Directory.GetDirectories(Application.dataPath, kRootUNEB, SearchOption.AllDirectories); if (dirs.Length != 0) { return dirs[0]; } else { Debug.LogError("Could not find project root: /" + kRootUNEB + '/'); return ""; } } // Assumes that the fullTempPath is valid. private static string getTempPathRelativeToAssets(string fullTempPath) { int index = fullTempPath.IndexOf("Assets"); return fullTempPath.Substring(index); } } }