mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-16 18:29:15 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
852 lines
24 KiB
C#
852 lines
24 KiB
C#
using Sandbox.Helpers;
|
|
using System;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace Editor;
|
|
|
|
public partial class SceneEditorSession
|
|
{
|
|
public UndoSystem UndoSystem { get; } = new UndoSystem();
|
|
|
|
internal bool IsUndoScopeOpen = false;
|
|
|
|
private void InitUndo()
|
|
{
|
|
UndoSystem.Initialize();
|
|
|
|
// annoy everyone as much as possible
|
|
UndoSystem.OnUndo = ( x ) =>
|
|
{
|
|
if ( EditorPreferences.UndoSounds )
|
|
{
|
|
EditorUtility.PlayRawSound( "sounds/editor/success.wav" );
|
|
}
|
|
|
|
HasUnsavedChanges = true;
|
|
};
|
|
UndoSystem.OnRedo = ( x ) =>
|
|
{
|
|
if ( EditorPreferences.UndoSounds )
|
|
{
|
|
EditorUtility.PlayRawSound( "sounds/editor/success.wav" );
|
|
}
|
|
|
|
HasUnsavedChanges = true;
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Take a full scene snapshot for the undo system. This is usually a last resort, if you can't do anything more incremental.
|
|
/// </summary>
|
|
[Obsolete( "Manual full scene undo snapshots are no longer working use UndoScope or AddUndo" )]
|
|
public void FullUndoSnapshot( string title )
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Push the current selection into the undo system
|
|
/// </summary>
|
|
[Obsolete( "Manual selections snapshots are no longer working use UndoScope or AddUndo" )]
|
|
public void PushUndoSelection()
|
|
{
|
|
}
|
|
|
|
[Obsolete( "EditLog is no longer working use UndoScope or AddUndo" )]
|
|
void Scene.ISceneEditorSession.OnEditLog( string name, object source )
|
|
{
|
|
}
|
|
|
|
public void AddUndo( string name, Action undo, Action redo )
|
|
{
|
|
UndoSystem.Insert( name, undo, redo );
|
|
}
|
|
|
|
public ISceneUndoScope UndoScope( string name )
|
|
{
|
|
return new SceneUndoScope( this, name );
|
|
}
|
|
}
|
|
|
|
internal sealed class SceneUndoSnapshot : IDisposable
|
|
{
|
|
sealed record ScopeSnapshot( JsonObject Scene, SelectionSnapshot Selection, ComponentSnapshot ComponentSnapshot, GameObjectSnapshot GameObjectSnapshot )
|
|
{
|
|
public bool Equals( ScopeSnapshot other )
|
|
{
|
|
if ( other == null )
|
|
return false;
|
|
if ( Scene == other.Scene &&
|
|
Selection == other.Selection &&
|
|
ComponentSnapshot == other.ComponentSnapshot &&
|
|
GameObjectSnapshot == other.GameObjectSnapshot )
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine( Scene, Selection, ComponentSnapshot, GameObjectSnapshot );
|
|
}
|
|
}
|
|
|
|
sealed record SelectionSnapshot
|
|
{
|
|
public readonly GameObjectReference[] SelectedGoRefs;
|
|
public readonly ComponentReference[] SelectedComponentRefs;
|
|
|
|
public struct ObjectReference
|
|
{
|
|
public Type Type;
|
|
public JsonNode Node;
|
|
}
|
|
|
|
public readonly ObjectReference[] SelectedObjectRefs;
|
|
private readonly SceneEditorSession _session;
|
|
|
|
public SelectionSnapshot( SceneEditorSession session )
|
|
{
|
|
_session = session;
|
|
|
|
SelectedGoRefs = _session.Selection.OfType<GameObject>().Select( GameObjectReference.FromInstance ).ToArray();
|
|
SelectedComponentRefs = _session.Selection.OfType<Component>().Select( ComponentReference.FromInstance ).ToArray();
|
|
SelectedObjectRefs = _session.Selection
|
|
.Where( x => x is not GameObject and not Component and not null )
|
|
.Select( x => new ObjectReference { Type = x.GetType(), Node = Json.ToNode( x ) } )
|
|
.ToArray();
|
|
}
|
|
|
|
public void Restore( Scene scene )
|
|
{
|
|
_session.Selection.Clear();
|
|
foreach ( var goRef in SelectedGoRefs )
|
|
{
|
|
var go = goRef.Resolve( scene, true );
|
|
if ( go.IsValid() )
|
|
{
|
|
_session.Selection.Add( go );
|
|
}
|
|
}
|
|
|
|
foreach ( var compRef in SelectedComponentRefs )
|
|
{
|
|
var comp = compRef.Resolve( scene );
|
|
if ( comp.IsValid() )
|
|
{
|
|
_session.Selection.Add( comp );
|
|
}
|
|
}
|
|
|
|
// We have not garantuee those are still exist and are valid but we try
|
|
foreach ( var objRef in SelectedObjectRefs )
|
|
{
|
|
var obj = Json.FromNode( objRef.Node, objRef.Type );
|
|
if ( obj is IValid x && !x.IsValid() )
|
|
{
|
|
continue;
|
|
}
|
|
_session.Selection.Add( obj );
|
|
}
|
|
}
|
|
|
|
public bool Equals( SelectionSnapshot other )
|
|
{
|
|
if ( other == null )
|
|
return false;
|
|
|
|
if ( SelectedGoRefs.Select( x => x.GameObjectId ).SequenceEqual( other.SelectedGoRefs.Select( x => x.GameObjectId ) ) &&
|
|
SelectedComponentRefs.Select( x => x.GameObjectId ).SequenceEqual( other.SelectedComponentRefs.Select( x => x.GameObjectId ) ) &&
|
|
SelectedObjectRefs.SequenceEqual( other.SelectedObjectRefs ) )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine( SelectedGoRefs, SelectedComponentRefs, SelectedObjectRefs );
|
|
}
|
|
}
|
|
|
|
sealed record ComponentSnapshot
|
|
{
|
|
public readonly JsonNode[] State;
|
|
public readonly ComponentReference[] ComponentRefs;
|
|
|
|
public ComponentSnapshot( IEnumerable<Component> components )
|
|
{
|
|
ComponentRefs = components.Select( ComponentReference.FromInstance ).ToArray();
|
|
|
|
var serializeOptions = new GameObject.SerializeOptions { };
|
|
State = components.Select( comp => comp.Serialize( serializeOptions ) ).ToArray();
|
|
}
|
|
|
|
public void Restore( Scene scene )
|
|
{
|
|
for ( int i = 0; i < State.Length; i++ )
|
|
{
|
|
Component comp = null;
|
|
comp = ComponentRefs[i].Resolve( scene );
|
|
|
|
if ( comp is null )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
comp.Deserialize( State[i].AsObject() );
|
|
}
|
|
}
|
|
|
|
public void PostRestore( Scene scene )
|
|
{
|
|
foreach ( var compRef in ComponentRefs )
|
|
{
|
|
var comp = compRef.Resolve( scene );
|
|
if ( comp.IsValid() )
|
|
{
|
|
compRef.Resolve( scene )?.PostDeserialize();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool Equals( ComponentSnapshot other )
|
|
{
|
|
if ( other == null )
|
|
return false;
|
|
if ( State == other.State &&
|
|
ComponentRefs.Select( x => x.ComponentId ).SequenceEqual( other.ComponentRefs.Select( x => x.ComponentId ) ) )
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine( State, ComponentRefs );
|
|
}
|
|
}
|
|
|
|
sealed record GameObjectSnapshot
|
|
{
|
|
public readonly List<JsonObject> State;
|
|
public readonly List<GameObjectReference> GameObjectRefs;
|
|
public readonly List<GameObjectReference> GameObjectNextSiblingRefs;
|
|
public readonly List<GameObjectReference> GameObjectParentRefs;
|
|
|
|
public GameObjectSnapshot( Dictionary<GameObject, GameObjectUndoFlags> gameObjects )
|
|
{
|
|
var goCount = gameObjects.Count;
|
|
GameObjectRefs = new( goCount );
|
|
State = new( goCount );
|
|
GameObjectNextSiblingRefs = new( goCount );
|
|
GameObjectParentRefs = new( goCount );
|
|
|
|
foreach ( var (go, flags) in gameObjects )
|
|
{
|
|
if ( !go.IsValid() )
|
|
{
|
|
Log.Info( $"Undo GameObjectSnapshot: GameObject queued for snapshot is not valid" );
|
|
continue;
|
|
}
|
|
var serializeOptions = new GameObject.SerializeOptions { IgnoreChildren = !flags.HasFlag( GameObjectUndoFlags.Children ), IgnoreComponents = !flags.HasFlag( GameObjectUndoFlags.Components ) };
|
|
GameObjectRefs.Add( GameObjectReference.FromInstance( go ) );
|
|
if ( go.IsOutermostPrefabInstanceRoot ) go.PrefabInstance.RefreshPatch();
|
|
State.Add( go.Serialize( serializeOptions ) );
|
|
GameObjectNextSiblingRefs.Add( go.GetNextSibling( false ).IsValid() ? GameObjectReference.FromInstance( go.GetNextSibling( false ) ) : GameObjectReference.FromId( Guid.Empty ) );
|
|
GameObjectParentRefs.Add( go.Parent.IsValid() ? GameObjectReference.FromInstance( go.Parent ) : GameObjectReference.FromId( Guid.Empty ) );
|
|
}
|
|
}
|
|
|
|
public void Restore( Scene scene, HashSet<GameObjectReference> createdGos = null )
|
|
{
|
|
for ( int i = 0; i < State.Count; i++ )
|
|
{
|
|
GameObject go = null;
|
|
if ( createdGos is not null && createdGos.Contains( GameObjectRefs[i] ) )
|
|
{
|
|
go = new GameObject();
|
|
// just need the id for now to ensure references are resolved when doing full deserialize later
|
|
go.DeserializeId( State[i] );
|
|
}
|
|
else
|
|
{
|
|
go = GameObjectRefs[i].Resolve( scene );
|
|
}
|
|
|
|
if ( go is null )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void PostRestore( Scene scene )
|
|
{
|
|
// second pass fully desiarizes and restores hierachy
|
|
for ( int i = 0; i < State.Count; i++ )
|
|
{
|
|
GameObject go = GameObjectRefs[i].Resolve( scene );
|
|
|
|
if ( go is null )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
go.Deserialize( State[i], new GameObject.DeserializeOptions { IsRefreshing = true } );
|
|
}
|
|
|
|
|
|
RestoreHierachy( scene );
|
|
}
|
|
|
|
private void RestoreHierachy( Scene scene )
|
|
{
|
|
// You could probably also sort the siblings in a smart way, but we will just brought force it in n^2
|
|
for ( int j = 0; j < State.Count; j++ )
|
|
{
|
|
for ( int i = 0; i < State.Count; i++ )
|
|
{
|
|
GameObject go = GameObjectRefs[i].Resolve( scene );
|
|
|
|
if ( go is null )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// restore position in scene hierachy
|
|
var prevSibling = GameObjectNextSiblingRefs[i].Resolve( scene );
|
|
var parent = GameObjectParentRefs[i].Resolve( scene );
|
|
if ( prevSibling is not null )
|
|
{
|
|
prevSibling.AddSibling( go, true, false );
|
|
}
|
|
else if ( parent is not null )
|
|
{
|
|
go.SetParent( parent, false );
|
|
}
|
|
else
|
|
{
|
|
go.SetParent( scene );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool Equals( GameObjectSnapshot other )
|
|
{
|
|
if ( other == null )
|
|
return false;
|
|
|
|
if ( State == other.State &&
|
|
GameObjectRefs.Select( x => x.GameObjectId ).SequenceEqual( other.GameObjectRefs.Select( x => x.GameObjectId ) ) &&
|
|
GameObjectNextSiblingRefs.Select( x => x.GameObjectId ).SequenceEqual( other.GameObjectNextSiblingRefs.Select( x => x.GameObjectId ) ) &&
|
|
GameObjectParentRefs.Select( x => x.GameObjectId ).SequenceEqual( other.GameObjectParentRefs.Select( x => x.GameObjectId ) ) )
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine( State, GameObjectNextSiblingRefs, GameObjectParentRefs );
|
|
}
|
|
}
|
|
|
|
private readonly SceneEditorSession _session;
|
|
private readonly string _name;
|
|
|
|
private ScopeSnapshot _initialState;
|
|
|
|
private HashSet<GameObject> _createdGameObjects = new();
|
|
|
|
private HashSet<Component> _createdComponents = new();
|
|
|
|
private Dictionary<GameObject, GameObjectUndoFlags> _initalCapturedGameObjects = new();
|
|
|
|
private HashSet<Component> _initialCapturedComponents = new();
|
|
|
|
private Dictionary<GameObject, GameObjectReference> _destroyedGameObjects { get; } = new();
|
|
|
|
private Dictionary<Component, ComponentReference> _destroyedComponents { get; } = new();
|
|
|
|
private bool _captureDestructions = false;
|
|
|
|
private bool _captureComponentCreations = false;
|
|
|
|
private bool _captureGameObjectCreations = false;
|
|
|
|
private bool _captureSelections = false;
|
|
|
|
private bool _captureComponentChanges => _initialCapturedComponents.Count > 0;
|
|
|
|
private bool _captureGameObjectChanges => _initalCapturedGameObjects.Count > 0;
|
|
|
|
internal SceneUndoSnapshot( SceneUndoScope builder )
|
|
{
|
|
_session = builder.Session;
|
|
|
|
using var sceneScope = _session.Scene.Push();
|
|
|
|
_name = builder.Name;
|
|
|
|
_session.IsUndoScopeOpen = true;
|
|
|
|
// resolve builder contents
|
|
foreach ( var (gos, flags) in builder.CapturedGameObjects )
|
|
{
|
|
foreach ( var go in gos )
|
|
{
|
|
// Need to capture the prefab root and only the prefab root if we edited an instance
|
|
if ( go.IsPrefabInstance )
|
|
{
|
|
_initalCapturedGameObjects[go.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
continue;
|
|
}
|
|
|
|
if ( _initalCapturedGameObjects.ContainsKey( go ) )
|
|
{
|
|
_initalCapturedGameObjects[go] |= flags;
|
|
}
|
|
else
|
|
{
|
|
_initalCapturedGameObjects[go] = flags;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
foreach ( var destroyedGo in builder.DestroyedGameObjects )
|
|
{
|
|
// if destroyed go is part of prefab we need to update it's instance cache
|
|
if ( destroyedGo.IsPrefabInstance && !destroyedGo.IsOutermostPrefabInstanceRoot )
|
|
{
|
|
_initalCapturedGameObjects[destroyedGo.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
}
|
|
// If we delete a nested instance we need to let a potential parent instance know and need to capture it.
|
|
else if ( destroyedGo.IsOutermostPrefabInstanceRoot && destroyedGo.Parent.IsValid() && destroyedGo.Parent.IsPrefabInstance )
|
|
{
|
|
_initalCapturedGameObjects[destroyedGo.Parent.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
}
|
|
|
|
_destroyedGameObjects[destroyedGo] = GameObjectReference.FromInstance( destroyedGo );
|
|
}
|
|
|
|
foreach ( var destroyedComp in builder.DestroyedComponents )
|
|
{
|
|
// if destroyed component is part of prefab we need to update it's instance cache
|
|
if ( destroyedComp.GameObject.IsPrefabInstance )
|
|
{
|
|
_initalCapturedGameObjects[destroyedComp.GameObject.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
}
|
|
_destroyedComponents[destroyedComp] = ComponentReference.FromInstance( destroyedComp );
|
|
}
|
|
|
|
foreach ( var comp in builder.CapturedComponents )
|
|
{
|
|
// Need to capture the prefab root and only the prefab root if we edited an instance
|
|
if ( comp.GameObject.IsPrefabInstance )
|
|
{
|
|
_initalCapturedGameObjects[comp.GameObject.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
continue;
|
|
}
|
|
|
|
// only add if parent is not already watched or does not have component flag
|
|
if ( !_initalCapturedGameObjects.ContainsKey( comp.GameObject ) || !_initalCapturedGameObjects[comp.GameObject].Contains( GameObjectUndoFlags.Components ) )
|
|
{
|
|
_initialCapturedComponents.Add( comp );
|
|
}
|
|
}
|
|
|
|
_captureDestructions = builder.CaptureDeletions;
|
|
_captureComponentCreations = builder.CaptureComponentCreations;
|
|
_captureGameObjectCreations = builder.CaptureGameObjectCreations;
|
|
_captureSelections = builder.CaptureSelections;
|
|
|
|
// Undo snapshots
|
|
|
|
JsonObject scene = null;
|
|
// if deletion is requested, we need to capture the whole scene
|
|
if ( _captureDestructions )
|
|
{
|
|
scene = _session.Scene.Serialize();
|
|
}
|
|
|
|
SelectionSnapshot selection = null;
|
|
if ( _captureSelections || _captureDestructions )
|
|
{
|
|
selection = new SelectionSnapshot( _session );
|
|
}
|
|
|
|
ComponentSnapshot componentSnapshot = null;
|
|
// If deletion was requested we already captured the whole scene, no point in capturing individual components
|
|
if ( !_captureDestructions && _initialCapturedComponents.Count > 0 )
|
|
{
|
|
componentSnapshot = new ComponentSnapshot( _initialCapturedComponents );
|
|
}
|
|
|
|
// If deletion was requested we already captured the whole scene, no point in capturing individual Fos
|
|
GameObjectSnapshot gameObjectSnapshot = null;
|
|
if ( !_captureDestructions && _initalCapturedGameObjects.Count > 0 )
|
|
{
|
|
gameObjectSnapshot = new GameObjectSnapshot( _initalCapturedGameObjects );
|
|
}
|
|
|
|
_initialState = new ScopeSnapshot( scene, selection, componentSnapshot, gameObjectSnapshot );
|
|
|
|
if ( _captureGameObjectCreations )
|
|
{
|
|
_session.Scene.Directory.OnGameObjectAdded += OnGameObjectAdded;
|
|
}
|
|
if ( _captureComponentCreations )
|
|
{
|
|
_session.Scene.Directory.OnComponentAdded += OnComponentAdded;
|
|
}
|
|
}
|
|
|
|
private bool _alreadyDisposed = false;
|
|
|
|
public void Dispose()
|
|
{
|
|
if ( _alreadyDisposed )
|
|
{
|
|
return;
|
|
}
|
|
|
|
using var sceneScope = _session.Scene.Push();
|
|
|
|
// Redo snapshots
|
|
|
|
// we don't capture the scene here again because that is never needed for redo
|
|
|
|
SelectionSnapshot selection = null;
|
|
if ( _captureSelections || _captureDestructions )
|
|
{
|
|
selection = new SelectionSnapshot( _session );
|
|
}
|
|
|
|
Dictionary<GameObject, GameObjectUndoFlags> disposeWatchedGameObjects = new();
|
|
|
|
// add all gos still valid and not destroyed
|
|
foreach ( var (go, flags) in _initalCapturedGameObjects )
|
|
{
|
|
if ( go.IsPrefabInstance )
|
|
{
|
|
disposeWatchedGameObjects[go.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
}
|
|
|
|
// We may have moved this object to a different prefab instance => update prefabroot instead of it
|
|
if ( go.Parent.IsValid() && go.Parent.IsPrefabInstance )
|
|
{
|
|
disposeWatchedGameObjects[go.Parent.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
continue;
|
|
}
|
|
if ( !go.IsValid() || go.IsDestroyed )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( !disposeWatchedGameObjects.ContainsKey( go ) )
|
|
{
|
|
disposeWatchedGameObjects.Add( go, flags );
|
|
}
|
|
else
|
|
{
|
|
disposeWatchedGameObjects[go] |= flags;
|
|
}
|
|
}
|
|
|
|
// add all created gos
|
|
HashSet<GameObjectReference> createdGameObjectRefs = new();
|
|
foreach ( var go in _createdGameObjects )
|
|
{
|
|
if ( !go.IsValid() || go.IsDestroyed )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Need to capture the prefab root and only the prefab root if we edited an instance
|
|
if ( go.IsPrefabInstance )
|
|
{
|
|
disposeWatchedGameObjects[go.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
// If we are a prefabinstanceroot and ahve been added to a prefab we also need to capture the outer prefab
|
|
if ( go.IsOutermostPrefabInstanceRoot && go.Parent.IsValid() && go.Parent.IsPrefabInstance )
|
|
{
|
|
disposeWatchedGameObjects[go.Parent.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
disposeWatchedGameObjects[go] = GameObjectUndoFlags.All;
|
|
}
|
|
createdGameObjectRefs.Add( GameObjectReference.FromInstance( go ) );
|
|
}
|
|
|
|
// for all components that have been created capture their parent go
|
|
HashSet<ComponentReference> createdComponentRefs = new();
|
|
foreach ( var component in _createdComponents )
|
|
{
|
|
if ( !component.IsValid() )
|
|
{
|
|
// can hapen if component was created and immediatly destoryed within undo scope
|
|
continue;
|
|
}
|
|
|
|
// Need to capture the prefab root and only the prefab root if we edited an instance
|
|
if ( component.GameObject.IsPrefabInstance )
|
|
{
|
|
disposeWatchedGameObjects[component.GameObject.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
}
|
|
else if ( disposeWatchedGameObjects.ContainsKey( component.GameObject ) )
|
|
{
|
|
disposeWatchedGameObjects[component.GameObject] |= GameObjectUndoFlags.Components;
|
|
}
|
|
else
|
|
{
|
|
disposeWatchedGameObjects[component.GameObject] = GameObjectUndoFlags.Components;
|
|
}
|
|
createdComponentRefs.Add( ComponentReference.FromInstance( component ) );
|
|
}
|
|
|
|
HashSet<Component> disposeWatchedComponents = new();
|
|
foreach ( var comp in _initialCapturedComponents )
|
|
{
|
|
if ( !comp.IsValid() )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Need to capture the prefab root and only the prefab root if we edited an instance
|
|
if ( comp.GameObject.IsPrefabInstance )
|
|
{
|
|
disposeWatchedGameObjects[comp.GameObject.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
|
|
}
|
|
// only add if parent is not already watched or does not have component flag
|
|
else if ( !_initalCapturedGameObjects.ContainsKey( comp.GameObject ) || !_initalCapturedGameObjects[comp.GameObject].Contains( GameObjectUndoFlags.Components ) )
|
|
{
|
|
disposeWatchedComponents.Add( comp );
|
|
}
|
|
}
|
|
|
|
ComponentSnapshot componentSnapshot = null;
|
|
if ( disposeWatchedComponents.Count > 0 )
|
|
{
|
|
componentSnapshot = new ComponentSnapshot( disposeWatchedComponents );
|
|
}
|
|
|
|
GameObjectSnapshot gameObjectSnapshot = null;
|
|
if ( disposeWatchedGameObjects.Count > 0 )
|
|
{
|
|
gameObjectSnapshot = new GameObjectSnapshot( disposeWatchedGameObjects );
|
|
}
|
|
|
|
var disposeState = new ScopeSnapshot( null, selection, componentSnapshot, gameObjectSnapshot );
|
|
|
|
var destroyedGameObjectRefs = _destroyedGameObjects.Select( x => x.Value ).ToArray();
|
|
var destroyedComponentRefs = _destroyedComponents.Select( x => x.Value ).ToArray();
|
|
|
|
var prefabInstanceRootsRequiringRefresh = new HashSet<GameObject>();
|
|
|
|
_session.Scene.Directory.OnComponentAdded -= OnComponentAdded;
|
|
_session.Scene.Directory.OnGameObjectAdded -= OnGameObjectAdded;
|
|
|
|
// if nothing changed, don't add an undo
|
|
if ( _initialState == disposeState )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// check if we have non selection changes
|
|
if ( _captureComponentChanges || _captureGameObjectChanges || _captureDestructions || _captureGameObjectCreations || _captureComponentCreations )
|
|
{
|
|
_session.HasUnsavedChanges = true;
|
|
}
|
|
|
|
// copy we want to avoid capture this
|
|
var preChangeStateCopy = _initialState;
|
|
_session.AddUndo( _name,
|
|
() =>
|
|
{
|
|
using var sceneScope = _session.Scene.Push();
|
|
|
|
// for undo we need to restore the state to the pre change state
|
|
// first check if we have a scene to restore
|
|
if ( preChangeStateCopy.Scene != null )
|
|
{
|
|
_session.Scene.Clear();
|
|
|
|
using ( CallbackBatch.Isolated() )
|
|
{
|
|
_session.Scene.Deserialize( preChangeStateCopy.Scene );
|
|
}
|
|
|
|
// do a tick immediately, to make sure everything is up to date
|
|
// if we don't do this we can get flickers when undo is called
|
|
_session.Scene.EditorTick( RealTime.Now, RealTime.Delta );
|
|
}
|
|
else
|
|
{
|
|
using var batch = CallbackBatch.Batch();
|
|
|
|
preChangeStateCopy.GameObjectSnapshot?.Restore( _session.Scene );
|
|
preChangeStateCopy.ComponentSnapshot?.Restore( _session.Scene );
|
|
|
|
preChangeStateCopy.ComponentSnapshot?.PostRestore( _session.Scene );
|
|
preChangeStateCopy.GameObjectSnapshot?.PostRestore( _session.Scene );
|
|
|
|
// delete created components
|
|
foreach ( var compRef in createdComponentRefs )
|
|
{
|
|
compRef.Resolve( _session.Scene )?.Destroy();
|
|
}
|
|
|
|
// delete created gos
|
|
foreach ( var goRef in createdGameObjectRefs )
|
|
{
|
|
goRef.Resolve( _session.Scene )?.Destroy();
|
|
}
|
|
}
|
|
|
|
// At last restore selection
|
|
preChangeStateCopy.Selection?.Restore( _session.Scene );
|
|
},
|
|
() =>
|
|
{
|
|
if ( disposeState.Scene != null )
|
|
{
|
|
throw new InvalidOperationException( "Redo should never use scene snapshots." );
|
|
}
|
|
|
|
using var sceneScope = _session.Scene.Push();
|
|
|
|
using var batch = CallbackBatch.Batch();
|
|
|
|
// delete destroyed components
|
|
foreach ( var compRef in destroyedComponentRefs )
|
|
{
|
|
compRef.Resolve( _session.Scene )?.Destroy();
|
|
}
|
|
|
|
// delete destroyed gos
|
|
foreach ( var goRef in destroyedGameObjectRefs )
|
|
{
|
|
goRef.Resolve( _session.Scene )?.Destroy();
|
|
}
|
|
|
|
// Restore Gos and pass created gos which need to be created first
|
|
disposeState.GameObjectSnapshot?.Restore( _session.Scene, createdGameObjectRefs );
|
|
disposeState.ComponentSnapshot?.Restore( _session.Scene );
|
|
|
|
// Do actual deserialization
|
|
disposeState.GameObjectSnapshot?.PostRestore( _session.Scene );
|
|
disposeState.ComponentSnapshot?.PostRestore( _session.Scene );
|
|
|
|
// restore selection
|
|
if ( disposeState.Selection != null )
|
|
{
|
|
disposeState.Selection.Restore( _session.Scene );
|
|
}
|
|
} );
|
|
|
|
_alreadyDisposed = true;
|
|
_session.IsUndoScopeOpen = false;
|
|
}
|
|
|
|
private void OnComponentAdded( Component comp )
|
|
{
|
|
_createdComponents.Add( comp );
|
|
}
|
|
|
|
private void OnGameObjectAdded( GameObject go )
|
|
{
|
|
_createdGameObjects.Add( go );
|
|
}
|
|
}
|
|
|
|
internal class SceneUndoScope : ISceneUndoScope
|
|
{
|
|
internal SceneEditorSession Session { get; }
|
|
internal string Name { get; }
|
|
internal bool CaptureSelections { get; private set; }
|
|
internal bool CaptureGameObjectCreations { get; private set; }
|
|
internal bool CaptureComponentCreations { get; private set; }
|
|
internal List<(GameObject[], GameObjectUndoFlags)> CapturedGameObjects { get; } = new();
|
|
internal List<Component> CapturedComponents { get; } = new();
|
|
internal List<GameObject> DestroyedGameObjects { get; } = new();
|
|
internal List<Component> DestroyedComponents { get; } = new();
|
|
internal bool CaptureDeletions => DestroyedGameObjects.Count > 0 || DestroyedComponents.Count > 0;
|
|
|
|
public SceneUndoScope( SceneEditorSession session, string name )
|
|
{
|
|
Session = session;
|
|
Name = name;
|
|
|
|
// Always capture selections
|
|
// If we ever want to make selections capture optional,
|
|
// it should be opt out (IngoreSelections) rather than opt in.
|
|
CaptureSelections = true;
|
|
}
|
|
public ISceneUndoScope WithGameObjectCreations()
|
|
{
|
|
CaptureGameObjectCreations = true;
|
|
return this;
|
|
}
|
|
public ISceneUndoScope WithGameObjectDestructions( IEnumerable<GameObject> gameObjects )
|
|
{
|
|
DestroyedGameObjects.AddRange( gameObjects );
|
|
return this;
|
|
}
|
|
public ISceneUndoScope WithGameObjectDestructions( GameObject gameObject )
|
|
{
|
|
DestroyedGameObjects.Add( gameObject );
|
|
return this;
|
|
}
|
|
public ISceneUndoScope WithGameObjectChanges( IEnumerable<GameObject> objects, GameObjectUndoFlags flags )
|
|
{
|
|
CapturedGameObjects.Add( (objects.ToArray(), flags) );
|
|
return this;
|
|
}
|
|
public ISceneUndoScope WithGameObjectChanges( GameObject gameObject, GameObjectUndoFlags flags )
|
|
{
|
|
CapturedGameObjects.Add( (new[] { gameObject }, flags) );
|
|
return this;
|
|
}
|
|
public ISceneUndoScope WithComponentCreations()
|
|
{
|
|
CaptureComponentCreations = true;
|
|
return this;
|
|
}
|
|
public ISceneUndoScope WithComponentDestructions( IEnumerable<Component> components )
|
|
{
|
|
DestroyedComponents.AddRange( components );
|
|
return this;
|
|
}
|
|
public ISceneUndoScope WithComponentDestructions( Component component )
|
|
{
|
|
DestroyedComponents.Add( component );
|
|
return this;
|
|
}
|
|
public ISceneUndoScope WithComponentChanges( IEnumerable<Component> components )
|
|
{
|
|
foreach ( var comp in components )
|
|
{
|
|
CapturedComponents.Add( comp );
|
|
}
|
|
return this;
|
|
}
|
|
public ISceneUndoScope WithComponentChanges( Component component )
|
|
{
|
|
CapturedComponents.Add( component );
|
|
return this;
|
|
}
|
|
public IDisposable Push()
|
|
{
|
|
var snapshot = new SceneUndoSnapshot( this );
|
|
return snapshot;
|
|
}
|
|
}
|