mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-20 04:10:00 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
484 lines
15 KiB
C#
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 );
|
|
}
|
|
}
|
|
}
|