using Facepunch.ActionGraphs; using Sandbox.ActionGraphs; using System; namespace Editor; /// /// A SceneEditorSession holds a Scene that is open in the editor. /// It creates a widget, has a selection and undo system. /// public partial class SceneEditorSession : Scene.ISceneEditorSession { /// /// All open editor sessions /// public static List All { get; } = new(); /// /// The editor session that is currently active /// public static SceneEditorSession Active { get; private set { if ( field == value ) return; field = value; field?.UpdateEditorTitle(); } } // scenes that have been edited, but waiting for a set interval to react // just to debounce changes. private static HashSet editedScenes = new(); /// /// Returns true if this session is editing a prefab /// public bool IsPrefabSession => this is PrefabEditorSession; /// /// The scene for this session /// public Scene Scene { get; private set; } internal Widget SceneDock { get; set; } protected SceneEditorSession( Scene scene ) { ArgumentNullException.ThrowIfNull( scene ); Scene = scene; Scene.Editor = this; All.Add( this ); InitUndo(); timeSinceSavedState = 0; if ( this is not GameEditorSession ) { // create dock - but not for game sessions, those are built into the parent session widget CreateSceneDock(); } EditorEvent.Register( this ); } /// /// Create the tabbed dock widget that holds the scene view /// void CreateSceneDock() { SceneDock = EditorTypeLibrary.Create( "SceneDock", new object[] { this } ); SceneDock.Name = $"SceneDock:{(Scene.Source?.ResourcePath ?? "untitled")}"; SetDockProperties(); SceneDock.Parent = EditorWindow; SceneDock.Visible = true; Dock(); } internal static void OnEditorWindowRestoreLayout() { // When we restore a layout it hides all windows and restores a default layout // Our currently open scenes are going to be in limbo with no area // So we need to show and dock them // Restoring will open a blank SceneDock as an area for the others to dock on var dummy = All.Where( x => EditorWindow.DockManager.IsDockOpen( x.SceneDock ) ).FirstOrDefault(); foreach ( var entry in All ) { if ( EditorWindow.DockManager.IsDockOpen( entry.SceneDock ) ) continue; entry.Dock(); } // Remove our dummy dock, unless it's the only one open somehow if ( All.Count > 1 ) dummy.Destroy(); } void Dock() { // Don't try to dock if we're being made by the DockManager (it will dock us after) if ( EditorWindow.DockManager._creatingDock ) return; // Dock inside the same area as other scenes (must be open) var siblingDock = All.Where( x => x != this && EditorWindow.DockManager.IsDockOpen( x.SceneDock ) ).FirstOrDefault(); if ( siblingDock is not null ) { EditorWindow.DockManager.AddDock( siblingDock.SceneDock, SceneDock, DockArea.Inside ); return; } // It should be impossible to have no scenes open, fail safe EditorWindow.DockManager.AddDock( null, SceneDock, DockArea.LastUsed ); } bool _destroyed; public virtual void Destroy() { if ( _destroyed ) return; _destroyed = true; // If this is the active scene // switch away to a sibling if ( this == Active ) { Active = null; var index = All.IndexOf( this ); if ( index >= 0 && All.Count > 1 ) { if ( index > 0 ) index--; else index++; Active = All[index]; } } All.Remove( this ); EditorEvent.Unregister( this ); Scene?.Destroy(); Scene = null; GameSession?.Destroy(); GameSession = null; SceneDock?.Destroy(); SceneDock = default; } /// /// Makes this scene active and brings it to the front /// public void MakeActive( bool bringToFront = true ) { Active = this; if ( bringToFront && EditorWindow is not null ) { BringToFront(); } } /// /// Bring this scene tab to the front /// public void BringToFront() { if ( EditorWindow.DockManager.IsDockOpen( SceneDock, false ) ) { EditorWindow.DockManager.RaiseDock( SceneDock ); } UpdateEditorTitle(); } RealTimeSince timeSinceSavedState; public void Tick() { // // If this is an editor scene, tick it to flush deleted objects etc // Scene.ProcessDeletes(); // Save camera state to disk if ( timeSinceSavedState > 1.0f ) { timeSinceSavedState = 0; EditorEvent.Run( "scene.session.save" ); } } [EditorEvent.Frame] private void SetDockProperties() { if ( !SceneDock.IsValid() ) return; var title = Scene.Name.Trim(); if ( IsPrefabSession ) { SceneDock.SetWindowIcon( "home_repair_service" ); SceneDock.WindowTitle = $"Prefab: {title}"; } else { SceneDock.SetWindowIcon( "grid_4x4" ); } } internal void UpdateEditorTitle() { if ( !SceneDock.IsValid() ) return; var name = Scene.Name.ToTitleCase().Trim(); if ( Scene.Editor?.HasUnsavedChanges ?? false ) name += "*"; EditorWindow?.UpdateEditorTitle( name ); if ( SceneDock is not null ) { SceneDock.WindowTitle = name; SceneDock.Name = $"SceneDock:{(Scene.Source?.ResourcePath ?? "untitled")}"; } } protected virtual void OnEdited() { } static RealTimeSince timeSinceLastUpdatePrefabs; internal static void ProcessSceneEdits() { if ( timeSinceLastUpdatePrefabs < 0.1 ) return; timeSinceLastUpdatePrefabs = 0; foreach ( var session in editedScenes.ToArray() ) { session.OnEdited(); } editedScenes.Clear(); } /// /// Pushes the active scene to the current scope /// public static IDisposable Scope() { return Active.Scene.Push(); } [Obsolete] void Scene.ISceneEditorSession.AddSelectionUndo() { PushUndoSelection(); } public Action OnFrameTo { get; set; } /// /// Zoom the scene to view this bbox /// public void FrameTo( in BBox box ) { BringToFront(); OnFrameTo?.Invoke( box ); } bool unsavedChanges; public bool HasUnsavedChanges { get => unsavedChanges; set { editedScenes.Add( this ); if ( unsavedChanges == value ) return; unsavedChanges = value; UpdateEditorTitle(); } } public void Reload() { if ( Scene.Source is null ) return; InitUndo(); Scene.Load( Scene.Source ); Selection.Clear(); } public void Save( bool saveAs ) { bool isPrefab = Scene is PrefabScene; var saveLocation = string.Empty; if ( Scene.Source is not null && AssetSystem.FindByPath( Scene.Source.ResourcePath ) is Asset sourceAsset ) { saveLocation = sourceAsset.AbsolutePath; } else { saveAs = true; } string extension = isPrefab ? "prefab" : "scene"; string fileType = isPrefab ? "Prefab" : "Scene"; if ( saveAs ) { if ( string.IsNullOrEmpty( saveLocation ) ) { saveLocation = System.IO.Path.Combine( System.IO.Path.GetDirectoryName( ProjectCookie.GetString( $"LastSaveLocation.{extension}", Project.Current.GetAssetsPath() ) ), $"untitled.{extension}" ); } saveLocation = EditorUtility.SaveFileDialog( $"Save {fileType} As..", extension, saveLocation ); if ( saveLocation is null ) return; ProjectCookie.SetString( $"LastSaveLocation.{extension}", System.IO.Path.GetDirectoryName( saveLocation ) ); } EditorEvent.Run( "scene.beforesave", SceneEditorSession.Active.Scene ); if ( Scene is PrefabScene prefabScene ) { var prefabFile = prefabScene.ToPrefabFile(); var asset = AssetSystem.CreateResource( "prefab", saveLocation ); asset.SaveToDisk( prefabFile ); // Update this scene's path Scene.Source = prefabFile; Scene.Name = System.IO.Path.GetFileNameWithoutExtension( saveLocation ); } else { var sceneFile = Scene.CreateSceneFile(); var asset = AssetSystem.CreateResource( "scene", saveLocation ); asset.SaveToDisk( sceneFile ); // Update this scene's path Scene.Source = sceneFile; Scene.Name = System.IO.Path.GetFileNameWithoutExtension( saveLocation ); } HasUnsavedChanges = false; EditorEvent.Run( "scene.saved", SceneEditorSession.Active.Scene ); UpdateEditorTitle(); } /// /// Resolve a scene to an editor session /// public static SceneEditorSession Resolve( Scene scene ) { return All.FirstOrDefault( x => x.Scene == scene ); } /// /// Resolve a scene file to an editor session. /// public static SceneEditorSession Resolve( SceneFile sceneFile ) { return All.FirstOrDefault( x => x is not null && !x.IsPrefabSession && string.Equals( sceneFile.ResourcePath, x.Scene.Source?.ResourcePath, StringComparison.OrdinalIgnoreCase ) ); } /// /// Resolve a prefab file to an editor session. /// public static SceneEditorSession Resolve( PrefabFile prefabFile ) { return All.FirstOrDefault( x => x is not null && x.IsPrefabSession && string.Equals( prefabFile.ResourcePath, x.Scene.Source?.ResourcePath, StringComparison.OrdinalIgnoreCase ) ); } /// /// Resolve an action graph source location to an editor session. /// public static SceneEditorSession Resolve( ISourceLocation sourceLocation ) { return sourceLocation switch { GameResourceSourceLocation { Resource: SceneFile sceneFile } => Resolve( sceneFile ), GameResourceSourceLocation { Resource: PrefabFile prefabFile } => Resolve( prefabFile ), _ => null }; } [Obsolete] public void RecordChange( SerializedProperty property ) { } /// /// Make a new SceneEditorSession with a default scene /// public static SceneEditorSession CreateDefault() { var scene = Scene.CreateEditorScene(); using var _ = scene.Push(); scene.Name = "Untitled Scene"; { var go = scene.CreateObject(); go.Name = "Main Camera"; go.LocalTransform = new Transform( Vector3.Up * 100 + Vector3.Backward * 300 ); go.Components.Create(); } { var go = scene.CreateObject(); go.Name = "Directional Light"; go.LocalTransform = new Transform( Vector3.Up * 200, Rotation.From( 80, 45, 0 ) ); go.Components.Create(); } return new SceneEditorSession( scene ); } /// /// Opens an editor session from an existing scene or prefab /// public static SceneEditorSession CreateFromPath( string path ) { var resource = ResourceLibrary.Get( path ); if ( resource is SceneFile sceneFile ) { if ( SceneEditorSession.Resolve( sceneFile ) is SceneEditorSession existingSession ) { existingSession.MakeActive(); return existingSession; } var openingScene = Scene.CreateEditorScene(); using var _ = openingScene.Push(); openingScene.Name = sceneFile.ResourceName.ToTitleCase(); openingScene.Load( sceneFile ); var session = new SceneEditorSession( openingScene ); return session; } if ( resource is PrefabFile prefabFile ) { if ( PrefabEditorSession.Resolve( prefabFile ) is PrefabEditorSession existingSession ) { existingSession.MakeActive(); return existingSession; } var openingScene = PrefabScene.CreateForEditing(); using var _ = openingScene.Push(); openingScene.Name = prefabFile.ResourceName.ToTitleCase(); openingScene.Load( prefabFile ); var session = new PrefabEditorSession( openingScene ); return session; } return null; } public IEnumerable GetSelection() { foreach ( var obj in Selection ) { yield return obj; } } }