Files
sbox-public/engine/Sandbox.Tools/Utility/Utility.Prefabs.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

484 lines
15 KiB
C#

using System;
namespace Editor;
public static partial class EditorUtility
{
public static partial class Prefabs
{
/// <summary>
/// Returns the name of the prefab file that this GameObject or Component is an instance of.
/// </summary>
public static bool IsOuterMostPrefabRoot( object obj )
{
var go = ResolveGameObject( obj );
if ( !go.IsValid() ) return false;
if ( !go.IsPrefabInstance ) return false;
return go.IsOutermostPrefabInstanceRoot;
}
/// <summary>
/// Returns the name of the prefab file that this GameObject or Component is an instance of.
/// </summary>
public static string GetOuterMostPrefabName( object obj )
{
var go = ResolveGameObject( obj );
if ( !go.IsValid() ) return null;
if ( !go.IsPrefabInstance ) return null;
var prefabName = System.IO.Path.GetFileNameWithoutExtension( go.OutermostPrefabInstanceRoot.PrefabInstanceSource );
return prefabName.ToTitleCase();
}
/// <summary>
/// <see cref="PrefabInstanceData.IsPropertyOverridden"/>
/// </summary>
public static bool IsPropertyOverridden( SerializedProperty prop )
{
var obj = prop.Parent;
if ( obj is null ) return false;
var go = obj.Targets.Select( ResolveGameObject ).FirstOrDefault();
var owner = obj.Targets.FirstOrDefault();
if ( !go.IsValid() ) return false;
if ( !go.IsPrefabInstance ) return false;
if ( !prop.IsEditable ) return false;
return go.OutermostPrefabInstanceRoot.PrefabInstance.IsPropertyOverridden( owner, prop.Name, go.IsPrefabInstanceRoot );
}
/// <summary>
/// <see cref="PrefabInstanceData.IsAddedGameObject"/>
/// </summary>
public static bool IsGameObjectAddedToInstance( GameObject go )
{
if ( !go.IsValid() ) return false;
if ( !go.IsPrefabInstance ) return false;
if ( go.IsOutermostPrefabInstanceRoot && go.Parent is not null && go.Parent.IsPrefabInstance )
{
return go.Parent.OutermostPrefabInstanceRoot.PrefabInstance.IsAddedGameObject( go );
}
return go.OutermostPrefabInstanceRoot.PrefabInstance.IsAddedGameObject( go );
}
/// <summary>
/// <see cref="PrefabInstanceData.IsAddedComponent"/>
/// </summary>
public static bool IsComponentAddedToInstance( Component comp )
{
if ( !comp.IsValid() ) return false;
if ( !comp.GameObject.IsPrefabInstance ) return false;
return comp.GameObject.OutermostPrefabInstanceRoot.PrefabInstance.IsAddedComponent( comp );
}
/// <summary>
/// <see cref="PrefabInstanceData.IsModified"/>
/// </summary>
public static bool IsInstanceModified( GameObject prefabInstance )
{
if ( !prefabInstance.IsValid() ) return false;
if ( !prefabInstance.IsPrefabInstanceRoot ) return false;
return prefabInstance.IsOutermostPrefabInstanceRoot && prefabInstance.OutermostPrefabInstanceRoot.PrefabInstance.IsModified();
}
/// <summary>
/// <see cref="PrefabInstanceData.IsGameObjectModified"/>
/// </summary>
public static bool IsGameObjectInstanceModified( GameObject go )
{
if ( !go.IsValid() ) return false;
if ( !go.IsPrefabInstance ) return false;
return go.OutermostPrefabInstanceRoot.PrefabInstance.IsGameObjectModified( go, go.IsOutermostPrefabInstanceRoot );
}
/// <summary>
/// <see cref="PrefabInstanceData.IsComponentModified"/>
/// </summary>
public static bool IsComponentInstanceModified( Component comp )
{
if ( !comp.GameObject.IsValid() ) return false;
if ( !comp.GameObject.IsPrefabInstance ) return false;
return comp.GameObject.OutermostPrefabInstanceRoot.PrefabInstance.IsComponentModified( comp );
}
/// <summary>
/// Returns true if the owning GameObject is part of a prefab instance.
/// </summary>
public static bool IsComponentPartOfInstance( Component comp )
{
if ( !comp.IsValid() ) return false;
if ( !comp.GameObject.IsValid() ) return false;
if ( !comp.GameObject.IsPrefabInstance ) return false;
return comp.GameObject.OutermostPrefabInstanceRoot.IsPrefabInstance;
}
/// <summary>
/// <see cref="PrefabInstanceData.RevertPropertyChange"/>
/// </summary>
public static void RevertPropertyChange( SerializedProperty prop )
{
var obj = prop.Parent;
if ( obj is null ) return;
var go = obj.Targets.Select( ResolveGameObject ).FirstOrDefault();
var owner = obj.Targets.FirstOrDefault();
if ( !go.IsValid() ) return;
if ( !go.IsPrefabInstance ) return;
go.OutermostPrefabInstanceRoot.PrefabInstance.RevertPropertyChange( owner, prop.Name );
}
/// <summary>
/// <see cref="PrefabInstanceData.ApplyPropertyChangeToPrefab"/>
/// </summary>
public static void ApplyPropertyChange( SerializedProperty prop )
{
var obj = prop.Parent;
if ( obj is null ) return;
var go = obj.Targets.Select( ResolveGameObject ).FirstOrDefault();
var owner = obj.Targets.FirstOrDefault();
if ( !go.IsValid() ) return;
if ( !go.IsPrefabInstance ) return;
// Store go id we use it to restore inspector
// Need to use id to restore inspector because prefab refresh may destroy references
var goId = go.Id;
go.OutermostPrefabInstanceRoot.PrefabInstance.ApplyPropertyChangeToPrefab( owner, prop.Name );
UpdatePrefabAfterModification( go.OutermostPrefabInstanceRoot.PrefabInstanceSource );
EditorUtility.InspectorObject = SceneEditorSession.Active.Scene.Directory.FindByGuid( goId );
}
/// <summary>
/// <see cref="PrefabInstanceData.RevertComponentChanges"/>
/// </summary>
public static void RevertComponentInstanceChanges( Component comp )
{
if ( !comp.IsValid() ) return;
if ( !comp.GameObject.IsPrefabInstance ) return;
comp.GameObject.OutermostPrefabInstanceRoot.PrefabInstance.RevertComponentChanges( comp );
}
/// <summary>
/// <see cref="PrefabInstanceData.RevertGameObjectChanges"/>
/// </summary>
public static void RevertGameObjectInstanceChanges( GameObject go )
{
if ( !go.IsValid() ) return;
if ( !go.IsPrefabInstance ) return;
go.OutermostPrefabInstanceRoot.PrefabInstance.RevertGameObjectChanges( go );
}
/// <summary>
/// <see cref="PrefabInstanceData.ApplyComponentChangesToPrefab"/>
/// </summary>
public static void ApplyComponentInstanceChangesToPrefab( Component comp )
{
if ( !comp.IsValid() ) return;
if ( !comp.GameObject.IsPrefabInstance ) return;
// Store go id we use it to restore inspector
// Need to use id to restore inspector because prefab refresh may destroy references
var goId = comp.GameObject.Id;
comp.GameObject.OutermostPrefabInstanceRoot.PrefabInstance.ApplyComponentChangesToPrefab( comp );
UpdatePrefabAfterModification( comp.GameObject.OutermostPrefabInstanceRoot.PrefabInstanceSource );
EditorUtility.InspectorObject = SceneEditorSession.Active.Scene.Directory.FindByGuid( goId );
}
/// <summary>
/// <see cref="PrefabInstanceData.AddGameObjectToPrefab"/>
/// </summary>
public static void AddInstanceAddedGameObjectToPrefab( GameObject go )
{
if ( !go.IsValid() ) return;
if ( !go.IsPrefabInstance ) return;
// Store go id we use it to restore inspector
// Need to use id to restore inspector because prefab refresh may destroy references
var goId = go.Id;
// Call AddGameObject on the correct prefab instance
// If go is an Added Prefab Instance
// we need to add it to the parent prefab instance
if ( go.IsOutermostPrefabInstanceRoot && go.Parent.IsValid() && go.Parent.IsPrefabInstance )
{
go.Parent.OutermostPrefabInstanceRoot.PrefabInstance.AddGameObjectToPrefab( go );
}
else
{
go.OutermostPrefabInstanceRoot.PrefabInstance.AddGameObjectToPrefab( go );
}
UpdatePrefabAfterModification( go.OutermostPrefabInstanceRoot.PrefabInstanceSource );
EditorUtility.InspectorObject = SceneEditorSession.Active.Scene.Directory.FindByGuid( goId );
}
/// <summary>
/// <see cref="PrefabInstanceData.ApplyGameObjectChangesToPrefab"/>
/// </summary>
public static void ApplyGameObjectInstanceChangesToPrefab( GameObject go )
{
if ( !go.IsValid() ) return;
if ( !go.IsPrefabInstance ) return;
// Store go id we use it to restore inspector
// Need to use id to restore inspector because prefab refresh may destroy references
var goId = go.Id;
go.OutermostPrefabInstanceRoot.PrefabInstance.ApplyGameObjectChangesToPrefab( go );
UpdatePrefabAfterModification( go.OutermostPrefabInstanceRoot.PrefabInstanceSource );
EditorUtility.InspectorObject = SceneEditorSession.Active.Scene.Directory.FindByGuid( goId );
}
/// <summary>
/// Revert a prefab instance to the state of the prefab.
/// </summary>
public static void RevertInstanceToPrefab( GameObject go )
{
if ( !go.IsPrefabInstance )
{
return;
}
go.OutermostPrefabInstanceRoot.PrefabInstance.ClearPatch( true );
go.UpdateFromPrefab();
}
/// <summary>
/// Write a prefab instance back to the prefab file and save it to disk.
/// </summary>
public static void WriteInstanceToPrefab( GameObject go, bool skipDiskWrite = false )
{
if ( !go.IsValid() ) return;
if ( !go.IsPrefabInstance ) return;
var prefabSource = go.OutermostPrefabInstanceRoot.PrefabInstanceSource;
WriteGameObjectToPrefab( go, prefabSource, skipDiskWrite );
}
private static (PrefabFile, Dictionary<Guid, Guid>) WriteGameObjectToPrefab( GameObject go, string saveLocation, bool skipDiskWrite = false )
{
if ( !go.IsValid() )
{
Log.Warning( "Failed to write GameObject to prefab, GameObject is invalid." );
return (null, null);
}
// We cannot allow writing prefabs to disk that contain themselves as instance.
if ( go.IsPrefabInstanceRoot )
{
foreach ( var prefabInstanceRoot in go.GetAllObjects( false ).Where( x => x.IsPrefabInstanceRoot ) )
{
if ( prefabInstanceRoot != go && prefabInstanceRoot.PrefabInstanceSource == go.PrefabInstanceSource )
{
Log.Warning( $"Failed to write GameObject to prefab, {prefabInstanceRoot.PrefabInstanceSource} occurs more than once in hierarchy." );
EditorUtility.PlayRawSound( "sounds/editor/fail.wav" );
return (null, null);
}
}
}
// Save instance transform for later
var instanceTransform = go.LocalTransform;
var prefabFile = ResourceLibrary.Get<PrefabFile>( saveLocation );
if ( !prefabFile.IsValid() )
{
prefabFile = new PrefabFile();
prefabFile.SetIdFromResourcePath( saveLocation );
prefabFile.Register( saveLocation );
}
Dictionary<Guid, Guid> instanceToPrefabGuid = null;
bool isWritingBackToExistingInstance = go.IsOutermostPrefabInstanceRoot && prefabFile.ResourcePath == go.PrefabInstanceSource;
// Some special care when writing back existing prefab instances
// Need to keep ids up to date
if ( isWritingBackToExistingInstance )
{
go.OutermostPrefabInstanceRoot.PrefabInstance.OverridePrefabWithInstance();
}
else
{
// Zero instance location for serialization to prefab file
go.LocalPosition = Vector3.Zero;
prefabFile.RootObject = go.SerializeStandard( new GameObject.SerializeOptions() );
instanceToPrefabGuid = SceneUtility.MakeIdGuidsUnique( prefabFile.RootObject );
// Reset transform
go.LocalTransform = instanceTransform;
}
UpdatePrefabAfterModification( prefabFile.ResourcePath, skipDiskWrite );
if ( isWritingBackToExistingInstance )
{
go.PrefabInstance.RefreshPatch();
}
return (prefabFile, instanceToPrefabGuid);
}
private static void WritePrefabToDisk( PrefabFile prefabFile, string saveLocation )
{
var asset = AssetSystem.FindByPath( saveLocation );
if ( asset is null )
{
asset = AssetSystem.CreateResource( "prefab", saveLocation );
}
asset.SaveToDisk( prefabFile );
}
private static void UpdatePrefabAfterModification( string prefabSource, bool skipFileWrite = false )
{
var prefabFile = ResourceLibrary.Get<PrefabFile>( prefabSource );
// Need to write back the changes to the editor session of this prefab if it exists, before updating the instances
var openSession = SceneEditorSession.All.OfType<PrefabEditorSession>().FirstOrDefault( session => session.Scene.Source.ResourcePath == prefabSource );
if ( openSession is not null )
{
using var undo = openSession.UndoScope( "Apply Prefab Instance Changes" )
.WithGameObjectChanges( openSession.Scene, GameObjectUndoFlags.All )
.Push();
openSession.Scene.Clear();
openSession.Scene.Deserialize( prefabFile.RootObject );
}
// TODO this only reason for skipFileWrite exists is because we cannot easily spinup an asset system in tests
if ( !skipFileWrite )
{
WritePrefabToDisk( prefabFile, prefabFile.ResourcePath );
}
}
/// <summary>
/// Convert a GameObject to a prefab. This will write the newly created prefab to disk and set the prefab source on the GameObject.
/// </summary>
public static void ConvertGameObjectToPrefab( GameObject go, string saveLocation, bool skipDiskWrite = false )
{
// We cannot convert the existing go in-place if it's part of a prefab instance.
if ( go.IsPrefabInstance )
{
var oldGo = go;
var oldGoSibling = go.GetNextSibling( false );
go = go.Clone();
if ( oldGoSibling != null )
{
oldGoSibling.AddSibling( go, true );
}
else
{
go.Parent = oldGo.Parent;
}
oldGo.OutermostPrefabInstanceRoot.PrefabInstance.RemoveHierarchyFromLookup( oldGo );
oldGo.Destroy();
}
var (prefabFile, instanceToPrefabGuid) = WriteGameObjectToPrefab( go, saveLocation, skipDiskWrite );
if ( prefabFile is null )
{
Log.Warning( "Failed to convert GameObject to prefab, could not write file." );
return;
}
// set or change prefab source
go.InitPrefabInstance( prefabFile.ResourcePath, false );
// Invert lookup
var prefabToInstanceGuid = new Dictionary<Guid, Guid>( instanceToPrefabGuid.Count );
foreach ( var kvp in instanceToPrefabGuid )
{
prefabToInstanceGuid[kvp.Value] = kvp.Key;
}
go.PrefabInstance.InitLookups( prefabToInstanceGuid );
go.PrefabInstance.RefreshPatch();
}
private static GameObject ResolveGameObject( object target )
{
if ( target is GameTransform gt ) return gt.GameObject;
if ( target is GameObject go ) return go;
if ( target is Component c ) return c.GameObject;
return default;
}
/// <summary>
/// Get a SerializedProperty representing variable targets. Will return null if there are no targets
/// </summary>
[Obsolete]
public static SerializedProperty GetTargets( GameObject root, PrefabVariable variable )
{
return null;
}
[Obsolete]
public static PrefabScene.VariableCollection GetVariables( SerializedObject obj )
{
return new PrefabScene.VariableCollection();
}
/// <summary>
/// Create a prefab out of any GameObject
/// </summary>
[Obsolete]
public static PrefabFile CreateAsset( GameObject clone )
{
return null;
}
/// <summary>
/// Fetches all prefab templates to show in Create GameObject menus
/// </summary>
public static IEnumerable<PrefabFile> GetTemplates()
{
return ResourceLibrary.GetAll<PrefabFile>()
.Where( x => x.IsValid() && x.ShowInMenu )
.GroupBy( x => x.MenuPath ?? string.Empty )
.Select( g => g.First() )
.OrderByDescending( x => (x.MenuPath ?? string.Empty).Count( c => c == '/' ) )
.ThenBy( x => x.MenuPath ?? string.Empty );
}
}
}