Files
sbox-public/engine/Sandbox.Test/Scene/GameObjects/Prefabs.Instances.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

1051 lines
42 KiB
C#

using Editor;
using Sandbox;
using Sandbox.Internal;
using System;
namespace GameObjects;
/// <summary>
/// Various prefab instance tests. No specific category.
/// </summary>
[TestClass]
public partial class PrefabInstances
{
[TestMethod]
public void WriteBackInstanceToPrefab()
{
var saveLocation = "___writeback.prefab";
using var prefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( saveLocation, _basicPrefabSource );
var pfile = ResourceLibrary.Get<PrefabFile>( saveLocation );
var prefabScene = SceneUtility.GetPrefabScene( pfile );
Assert.IsTrue( prefabScene.Components.Get<ModelRenderer>() is not null );
// spawn in scene
var scene = new Scene();
using var sceneScope = scene.Push();
Assert.IsNull( scene.Camera );
Assert.AreEqual( scene.Directory.Count, 0 );
var instance = prefabScene.Clone( Vector3.Right * -100 );
Assert.AreEqual( scene.Directory.Count, 1 );
Assert.IsTrue( instance.Components.Get<ModelRenderer>() is not null );
instance.AddComponent<SkinnedModelRenderer>();
Assert.IsTrue( instance.Components.Get<SkinnedModelRenderer>() is not null );
instance.AddComponent<ModelHitboxes>();
Assert.IsTrue( instance.Components.Get<ModelHitboxes>() is not null );
instance.Components.Get<ModelHitboxes>().Renderer = instance.Components.Get<SkinnedModelRenderer>();
EditorUtility.Prefabs.WriteInstanceToPrefab( instance, true );
Assert.AreEqual( 3, prefabScene.Components.Count );
Assert.IsTrue( prefabScene.Components.Get<SkinnedModelRenderer>() is not null );
Assert.AreNotEqual( instance.Components.Get<ModelRenderer>().Id, prefabScene.Components.Get<ModelRenderer>().Id );
// Added component should also not share an id with the prefab to avoid conflicts
Assert.AreNotEqual( instance.Components.Get<SkinnedModelRenderer>().Id, prefabScene.Components.Get<SkinnedModelRenderer>().Id );
// Reference to added objects should be maintaned, when writing back to prefab
Assert.AreEqual( instance.Components.Get<ModelHitboxes>().Renderer.Id, instance.Components.Get<SkinnedModelRenderer>().Id );
Assert.AreNotEqual( instance.Components.Get<ModelHitboxes>().Renderer.Id, prefabScene.Components.Get<SkinnedModelRenderer>().Id );
Assert.AreEqual( prefabScene.Components.Get<ModelHitboxes>().Renderer.Id, prefabScene.Components.Get<SkinnedModelRenderer>().Id );
}
[TestMethod]
public void UpstreamChangesFromPrefabToInstance()
{
var saveLocation = "___upstream_changes.prefab";
// Create our base prefab
using var prefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( saveLocation, _basicPrefabSource );
var pfile = ResourceLibrary.Get<PrefabFile>( saveLocation );
var prefabScene = SceneUtility.GetPrefabScene( pfile );
Assert.IsTrue( prefabScene.Components.Get<ModelRenderer>() is not null );
// Create an instance in a scene
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone( Vector3.Zero );
// Verify the instance has the expected component from prefab
Assert.IsTrue( instance.Components.Get<ModelRenderer>() is not null );
// Make a local modification to the instance (this should be preserved)
instance.Components.Get<ModelRenderer>().Tint = Color.Blue;
// The editor would run this after a modification
instance.PrefabInstance.RefreshPatch();
// Now modify the original prefab by adding a new component
prefabScene.AddComponent<BoxCollider>();
Assert.IsTrue( prefabScene.Components.Get<BoxCollider>() is not null );
// Save the changes to the prefab file
prefabScene.ToPrefabFile();
// Update the instance from the modified prefab
instance.UpdateFromPrefab();
// Verify that:
// 1. The instance now has the new component from the prefab
Assert.IsTrue( instance.Components.Get<BoxCollider>() is not null );
// 2. The instance's local modifications are preserved
Assert.AreEqual( Color.Blue, instance.Components.Get<ModelRenderer>().Tint );
// 3. The original prefab's component values remain unchanged
prefabScene.Load( pfile ); // Refresh to be sure we have the latest data
Assert.AreEqual( new Color( 1, 0, 0, 1 ), prefabScene.Components.Get<ModelRenderer>().Tint );
}
static readonly string _basicPrefabSource = """"
{
"__guid": "fab370f8-2e2c-48cf-a523-e4be49723490",
"Name": "Object",
"Position": "788.8395,-1793.604,-1218.092",
"Scale": "10, 10, 10",
"Enabled": true,
"Components": [
{
"__type": "ModelRenderer",
"__guid": "230b45c1-a446-42b4-af39-f7195135e31f",
"BodyGroups": 18446744073709551615,
"MaterialGroup": null,
"MaterialOverride": null,
"Model": null,
"RenderType": "On",
"Tint": "1,0,0,1"
}
],
"Children": []
}
"""";
[TestMethod]
public void UpstreamChangesWithHierarchiesThatContainTheSamePrefabMoreThanOnce_Nested()
{
var saveLocation = "___upstream_changes_self_nested.prefab";
// Create our base prefab
using var prefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( saveLocation, _basicPrefabSource );
var pfile = ResourceLibrary.Get<PrefabFile>( saveLocation );
var prefabScene = SceneUtility.GetPrefabScene( pfile );
Assert.IsTrue( prefabScene.Components.Get<ModelRenderer>() is not null );
// Create an instance in a scene
var scene = new Scene();
using var sceneScope = scene.Push();
var hierarchy = scene.CreateObject();
hierarchy.Deserialize( Json.ParseToJsonObject( _gameObjectWithNestedPrefabInstance ) );
// Verify the instance has the expected component from prefab
Assert.AreEqual( 1, hierarchy.Children.Count );
GameObject outerPrefabInstance = hierarchy.Children[0];
Assert.IsTrue( outerPrefabInstance.Components.Get<ModelRenderer>() is not null );
// Make a local modification to the instance (this should be preserved)
outerPrefabInstance.Components.Get<ModelRenderer>().Tint = Color.Blue;
// The editor would run this after a modification
outerPrefabInstance.PrefabInstance.RefreshPatch();
// Now modify the original prefab by adding a new component
prefabScene.AddComponent<BoxCollider>();
Assert.IsTrue( prefabScene.Components.Get<BoxCollider>() is not null );
// Save the changes to the prefab file
prefabScene.ToPrefabFile();
// Update the instance from the modified prefab
// This should call RefreshCachedPatch with refreshFromUpstream=true internally
outerPrefabInstance.UpdateFromPrefab();
// Verify that:
// 1. The instance now has the new component from the prefab
Assert.IsTrue( outerPrefabInstance.Components.Get<BoxCollider>() is not null );
// 2. The instance's local modifications are preserved
Assert.AreEqual( Color.Blue, outerPrefabInstance.Components.Get<ModelRenderer>().Tint );
// 3. The original prefab's component values remain unchanged
prefabScene.Load( pfile ); // Refresh to be sure we have the latest data
Assert.AreEqual( new Color( 1, 0, 0, 1 ), prefabScene.Components.Get<ModelRenderer>().Tint );
}
static readonly string _gameObjectWithNestedPrefabInstance = """"
{
"__guid": "d03c7897-8558-48ee-990f-547b61de7f9a",
"Name": "Object",
"Position": "788.8395,-1793.604,-1218.092",
"Enabled": true,
"Children": [
{
"__guid": "c07c9e05-a64f-4354-a98f-11bbdbec71fa",
"__version": 1,
"__Prefab": "___upstream_changes_self_nested.prefab",
"__PrefabInstancePatch": {
"AddedObjects": [
{
"Id": {
"Type": "GameObject",
"IdValue": "e5156285-adb9-4986-bd03-e7899750ffe1"
},
"Parent": {
"Type": "GameObject",
"IdValue": "fab370f8-2e2c-48cf-a523-e4be49723490"
},
"PreviousElement": null,
"ContainerProperty": "Children",
"IsContainerArray": true,
"Data": {
"__guid": "e5156285-adb9-4986-bd03-e7899750ffe1",
"__version": 1,
"__Prefab": "___upstream_changes_self_nested.prefab",
"__PrefabInstancePatch": {
"AddedObjects": [
{
"Id": {
"Type": "GameObject",
"IdValue": "3508f8c0-9cb0-421b-a040-9d340c5611c1"
},
"Parent": {
"Type": "GameObject",
"IdValue": "fab370f8-2e2c-48cf-a523-e4be49723490"
},
"PreviousElement": null,
"ContainerProperty": "Children",
"IsContainerArray": true,
"Data": {
"__guid": "3508f8c0-9cb0-421b-a040-9d340c5611c1",
"__version": 1,
"__Prefab": "___upstream_changes_self_nested.prefab",
"__PrefabInstancePatch": {
"AddedObjects": [],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"fab370f8-2e2c-48cf-a523-e4be49723490": "3508f8c0-9cb0-421b-a040-9d340c5611c1",
"230b45c1-a446-42b4-af39-f7195135e31f": "d2679da4-2b84-4aa5-95ad-32e47a1bbed5"
}
}
}
],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"fab370f8-2e2c-48cf-a523-e4be49723490": "e5156285-adb9-4986-bd03-e7899750ffe1",
"230b45c1-a446-42b4-af39-f7195135e31f": "65886a24-2a05-41c7-a5dc-da429ea1aeff"
}
}
}
],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"fab370f8-2e2c-48cf-a523-e4be49723490": "c07c9e05-a64f-4354-a98f-11bbdbec71fa",
"230b45c1-a446-42b4-af39-f7195135e31f": "19e18ce9-aba4-465c-b07b-7e63f5f05cef"
}
}
]
}
"""";
[TestMethod]
public void UpstreamChangesWithHierarchiesThatContainTheSamePrefabMoreThanOnce()
{
var saveLocation = "___upstream_changes_multiple.prefab";
// Create our base prefab
using var prefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( saveLocation, _basicPrefabSource );
var pfile = ResourceLibrary.Get<PrefabFile>( saveLocation );
var prefabScene = SceneUtility.GetPrefabScene( pfile );
Assert.IsTrue( prefabScene.Components.Get<ModelRenderer>() is not null );
// Create an instance in a scene
var scene = new Scene();
using var sceneScope = scene.Push();
var hierarchy = scene.CreateObject();
hierarchy.Deserialize( Json.ParseToJsonObject( _gameObjectWithPrefabInstances ) );
// Verify the instance has the expected component from prefab
Assert.AreEqual( 2, hierarchy.Children.Count );
foreach ( var prefabInstance in hierarchy.Children )
{
Assert.IsTrue( prefabInstance.Components.Get<ModelRenderer>() is not null );
// Make a local modification to the instance (this should be preserved)
prefabInstance.Components.Get<ModelRenderer>().Tint = Color.Blue;
// The editor would run this after a modification
prefabInstance.PrefabInstance.RefreshPatch();
}
// Now modify the original prefab by adding a new go and component
var newGo = prefabScene.CreateObject();
newGo.AddComponent<BoxCollider>();
Assert.IsTrue( newGo.Components.Get<BoxCollider>() is not null );
prefabScene.ToPrefabFile();
foreach ( var prefabInstance in hierarchy.Children )
{
// Update the instance from the modified prefab
prefabInstance.UpdateFromPrefab();
Assert.AreEqual( 1, prefabInstance.Children.Count );
Assert.IsNotNull( prefabInstance.Children[0].Components.Get<BoxCollider>() );
Assert.AreEqual( Color.Blue, prefabInstance.Components.Get<ModelRenderer>().Tint );
}
prefabScene.Load( pfile ); // Refresh to be sure we have the latest data
Assert.AreEqual( new Color( 1, 0, 0, 1 ), prefabScene.Components.Get<ModelRenderer>().Tint );
}
static readonly string _gameObjectWithPrefabInstances = """"
{
"__guid": "d03c7897-8558-48ee-990f-547b61de7f9a",
"Name": "Object",
"Position": "788.8395,-1793.604,-1218.092",
"Enabled": true,
"Children": [
{
"__guid": "c07c9e05-a64f-4354-a98f-11bbdbec71fa",
"__version": 1,
"__Prefab": "___upstream_changes_multiple.prefab",
"__PrefabInstancePatch": {
"AddedObjects": [],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"fab370f8-2e2c-48cf-a523-e4be49723490": "c07c9e05-a64f-4354-a98f-11bbdbec71fa",
"230b45c1-a446-42b4-af39-f7195135e31f": "19e18ce9-aba4-465c-b07b-7e63f5f05cef"
}
},
{
"__guid": "3508f8c0-9cb0-421b-a040-9d340c5611c1",
"__version": 1,
"__Prefab": "___upstream_changes_multiple.prefab",
"__PrefabInstancePatch": {
"AddedObjects": [],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"fab370f8-2e2c-48cf-a523-e4be49723490": "3508f8c0-9cb0-421b-a040-9d340c5611c1",
"230b45c1-a446-42b4-af39-f7195135e31f": "d2679da4-2b84-4aa5-95ad-32e47a1bbed5"
}
}
]
}
"""";
[TestMethod]
public void SelfNestedPrefabInstanceWithAddedGameObjectInNestedInstanceShouldCopyCorrectly()
{
var saveLocation = "___upstream_changes_self_nested.prefab";
// Create our base prefab
using var prefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( saveLocation, _basicPrefabSource );
var pfile = ResourceLibrary.Get<PrefabFile>( saveLocation );
var prefabScene = SceneUtility.GetPrefabScene( pfile );
Assert.IsTrue( prefabScene.Components.Get<ModelRenderer>() is not null );
// Create an instance in a scene
var scene = new Scene();
using var sceneScope = scene.Push();
var hierarchy = scene.CreateObject();
hierarchy.Deserialize( Json.ParseToJsonObject( _gameObjectWithNestedPrefabInstance ) );
// Add a new gameobject to the nested prefab instance
var nestedPrefabInstance = hierarchy.Children[0].Children[0];
Assert.IsNotNull( nestedPrefabInstance.Components.Get<ModelRenderer>() );
Assert.AreEqual( 1, nestedPrefabInstance.Children.Count );
var addedObject = new GameObject( nestedPrefabInstance );
// Editor would make sure this is called
nestedPrefabInstance.PrefabInstance.RefreshPatch();
// Verify that the added object is now part of the prefab instance
Assert.AreEqual( 2, nestedPrefabInstance.Children.Count );
// Emulate copy paste
var s = hierarchy.Serialize();
var newHierarchy = scene.CreateObject();
SceneUtility.MakeIdGuidsUnique( s );
newHierarchy.Deserialize( s );
// Verify that the new hierarchy has the same structure
Assert.AreEqual( 1, newHierarchy.Children.Count );
var newNestedPrefabInstance = newHierarchy.Children[0].Children[0];
Assert.AreEqual( 2, newNestedPrefabInstance.Children.Count );
Assert.IsNotNull( newNestedPrefabInstance.Components.Get<ModelRenderer>() );
}
[TestMethod]
public void SelfNestedPrefabInstanceWithReferenceToOuterInstanceInNestedInstanceShouldCopyCorrectly()
{
var saveLocation = "___upstream_changes_self_nested.prefab";
// Create our base prefab
using var prefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( saveLocation, _basicPrefabSource );
var pfile = ResourceLibrary.Get<PrefabFile>( saveLocation );
var prefabScene = SceneUtility.GetPrefabScene( pfile );
Assert.IsTrue( prefabScene.Components.Get<ModelRenderer>() is not null );
// Create an instance in a scene
var scene = new Scene();
using var sceneScope = scene.Push();
var hierarchy = scene.CreateObject();
hierarchy.Deserialize( Json.ParseToJsonObject( _gameObjectWithNestedPrefabInstance ) );
// Add a new gameobject to the nested prefab instance
var nestedPrefabInstance = hierarchy.Children[0].Children[0];
Assert.IsNotNull( nestedPrefabInstance.Components.Get<ModelRenderer>() );
Assert.AreEqual( 1, nestedPrefabInstance.Children.Count );
var lineRenderer = nestedPrefabInstance.AddComponent<LineRenderer>();
lineRenderer.Points = [hierarchy.Children[0]];
// Editor would make sure this is called
nestedPrefabInstance.PrefabInstance.RefreshPatch();
// Emulate copy paste
var s = hierarchy.Serialize();
var newHierarchy = scene.CreateObject();
SceneUtility.MakeIdGuidsUnique( s );
newHierarchy.Deserialize( s );
// Verify that the copied line renderer is referencing the outer instance of the new hierarchy
var newNestedPrefabInstance = newHierarchy.Children[0].Children[0];
Assert.AreEqual( 1, newNestedPrefabInstance.Children.Count );
Assert.IsNotNull( newNestedPrefabInstance.Components.Get<LineRenderer>() );
var newLineRenderer = newNestedPrefabInstance.Components.Get<LineRenderer>();
Assert.AreEqual( 1, newLineRenderer.Points.Count );
Assert.AreEqual( newLineRenderer.Points[0], newHierarchy.Children[0] );
}
[TestMethod]
public void UpdatesToNestedPrefabFileShouldPropagateToOuterPrefabFile()
{
var nestedPrefabLocation = "__nestedPrefab.prefab";
var outerPrefabLocation = "__outerPrefab.prefab";
// Step 1: Create the nested prefab
using var nestedPrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( nestedPrefabLocation, _basicPrefabSource );
var nestedPrefabFile = ResourceLibrary.Get<PrefabFile>( nestedPrefabLocation );
var nestedPrefabScene = SceneUtility.GetPrefabScene( nestedPrefabFile );
Assert.IsTrue( nestedPrefabScene.Components.Get<ModelRenderer>() is not null );
// Step 2: Create the outer prefab containing the nested prefab
using var outerPrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( outerPrefabLocation, _outerPrefabWithNestedPrefabSource );
var outerPrefabFile = ResourceLibrary.Get<PrefabFile>( outerPrefabLocation );
var outerPrefabScene = SceneUtility.GetPrefabScene( outerPrefabFile );
// Verify the outer prefab structure and its nested prefab instance
Assert.AreEqual( 1, outerPrefabScene.Children.Count );
var nestedPrefabInstance = outerPrefabScene.Children[0];
Assert.IsTrue( nestedPrefabInstance.PrefabInstance != null );
Assert.IsTrue( nestedPrefabInstance.Components.Get<ModelRenderer>() is not null );
// Step 3: Modify the nested prefab by adding a new component
nestedPrefabScene.AddComponent<BoxCollider>();
Assert.IsTrue( nestedPrefabScene.Components.Get<BoxCollider>() is not null );
// Step 4: Save the changes to the nested prefab file
nestedPrefabScene.ToPrefabFile();
// Step 5: Verify that the changes in the nested prefab are reflected in the outer prefab
// The nested prefab instance should now have the BoxCollider
Assert.AreEqual( 1, outerPrefabScene.Children.Count );
var updatedNestedPrefabInstance = outerPrefabScene.Children[0];
// This is the key assertion - the BoxCollider from the nested prefab should now appear
// in the instance within the outer prefab
Assert.IsTrue( updatedNestedPrefabInstance.Components.Get<BoxCollider>() is not null );
}
[TestMethod]
public void OutermostPrefabInstanceRoot_FromNestedPrefabShouldReturnCorrectRoot()
{
// Create the nested structure:
// - Inner prefab (basic prefab)
// - Outer prefab (contains an instance of inner prefab)
var innerPrefabLocation = "__nestedPrefab.prefab";
var outerPrefabLocation = "__outer_prefab.prefab";
// Step 1: Create the inner prefab
using var innerPrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( innerPrefabLocation, _basicPrefabSource );
var innerPrefabFile = ResourceLibrary.Get<PrefabFile>( innerPrefabLocation );
var innerPrefabScene = SceneUtility.GetPrefabScene( innerPrefabFile );
// Step 2: Create the outer prefab containing the inner prefab
using var outerPrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( outerPrefabLocation, _outerPrefabWithNestedPrefabSource );
var outerPrefabFile = ResourceLibrary.Get<PrefabFile>( outerPrefabLocation );
var outerPrefabScene = SceneUtility.GetPrefabScene( outerPrefabFile );
// Step 3: Create a scene and instantiate the outer prefab
var scene = new Scene();
using var sceneScope = scene.Push();
var outerInstance = outerPrefabScene.Clone( Vector3.Zero );
Assert.IsTrue( outerInstance.IsPrefabInstanceRoot );
// Step 4: Get the inner prefab instance within the outer prefab
var innerInstance = outerInstance.Children[0];
Assert.IsTrue( innerInstance.IsPrefabInstanceRoot );
// Get a component inside the inner instance to test from
var modelRenderer = innerInstance.Components.Get<ModelRenderer>();
Assert.IsNotNull( modelRenderer );
// Step 5: Test OutermostPrefabInstanceRoot from different levels
// Test from innerInstance - should return outerInstance
Assert.AreEqual( outerInstance, innerInstance.OutermostPrefabInstanceRoot );
// Test from outerInstance - should return itself
Assert.AreEqual( outerInstance, outerInstance.OutermostPrefabInstanceRoot );
// Test IsOutermostPrefabInstanceRoot property
Assert.IsTrue( outerInstance.IsOutermostPrefabInstanceRoot );
Assert.IsFalse( innerInstance.IsOutermostPrefabInstanceRoot );
}
[TestMethod]
public void OutermostPrefabInstanceRoot_RuntimeAddedPrefabShouldReturnItself()
{
// Step 1: Create two prefabs
var prefabALocation = "__prefab_a.prefab";
var prefabBLocation = "__prefab_b.prefab";
using var prefabA = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( prefabALocation, _basicPrefabSource );
var prefabAFile = ResourceLibrary.Get<PrefabFile>( prefabALocation );
var prefabAScene = SceneUtility.GetPrefabScene( prefabAFile );
// Create prefab B with a different tint color to distinguish it
string prefabBSource = _basicPrefabSource.Replace( "1,0,0,1", "0,1,0,1" );
using var prefabB = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( prefabBLocation, prefabBSource );
var prefabBFile = ResourceLibrary.Get<PrefabFile>( prefabBLocation );
var prefabBScene = SceneUtility.GetPrefabScene( prefabBFile );
// Step 2: Create a scene and instantiate prefab A
var scene = new Scene();
using var sceneScope = scene.Push();
var instanceA = prefabAScene.Clone( Vector3.Zero );
Assert.IsTrue( instanceA.IsPrefabInstanceRoot );
Assert.IsTrue( instanceA.IsOutermostPrefabInstanceRoot );
// Step 3: At runtime, instantiate prefab B and parent it to instanceA
var instanceB = prefabBScene.Clone( Vector3.Zero );
instanceB.SetParent( instanceA );
// Editor would run this
instanceA.PrefabInstance.RefreshPatch();
// Even though instanceB is now a child of instanceA,
// it should still be its own outermost prefab instance root
// because it was added at runtime and not part of the original prefab
// Test from instanceB
Assert.AreEqual( instanceB, instanceB.OutermostPrefabInstanceRoot );
Assert.IsTrue( instanceB.IsOutermostPrefabInstanceRoot );
// Step 4: Create a new GameObject inside instanceB
var childOfInstanceB = scene.CreateObject();
childOfInstanceB.SetParent( instanceB );
// This child is part of instanceB, so its outermost prefab instance root should be instanceB
Assert.AreEqual( instanceB, childOfInstanceB.OutermostPrefabInstanceRoot );
// Step 5: Create a new GameObject inside instanceA (but not part of instanceB)
var childOfInstanceA = scene.CreateObject();
childOfInstanceA.SetParent( instanceA );
// This child is part of instanceA, so its outermost prefab instance root should be instanceA
Assert.AreEqual( instanceA, childOfInstanceA.OutermostPrefabInstanceRoot );
// Verify the hierarchy
Assert.AreEqual( 2, instanceA.Children.Count ); // instanceB and childOfInstanceA
Assert.AreEqual( 1, instanceB.Children.Count ); // childOfInstanceB
}
static readonly string _outerPrefabWithNestedPrefabSource = """"
{
"__guid": "16a942f3-8b7c-4b6e-a14b-5854d568e256",
"Name": "OuterPrefab",
"Position": "0,0,0",
"Enabled": true,
"Components": [
{
"__type": "ModelRenderer",
"__guid": "b34c25d6-22cd-4e4a-9fb4-71c12dce2efd",
"BodyGroups": 18446744073709551615,
"MaterialGroup": null,
"MaterialOverride": null,
"Model": null,
"RenderType": "On",
"Tint": "0,1,0,1"
}
],
"Children": [
{
"__guid": "f1482e7a-a10c-4c5b-b0fa-3dc07ef5f7e9",
"__version": 1,
"__Prefab": "__nestedPrefab.prefab",
"__PrefabInstancePatch": {
"AddedObjects": [],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"fab370f8-2e2c-48cf-a523-e4be49723490": "f1482e7a-a10c-4c5b-b0fa-3dc07ef5f7e9",
"230b45c1-a446-42b4-af39-f7195135e31f": "aa721c3b-9d6c-48d5-81a9-f72ef5c5b12e"
}
}
]
}
"""";
private void TestOverriddenPropertyOnPrefabInstance(
string saveLocation,
string prefabSource,
Action<GameObject> assertInitialState,
Action<GameObject> setOverrideAction,
Action<GameObject> assertOverrideSet,
Action<GameObject> assertOverridePreserved
)
{
// Create the prefab from JSON source
using var prefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( saveLocation, prefabSource );
var pfile = ResourceLibrary.Get<PrefabFile>( saveLocation );
var prefabScene = SceneUtility.GetPrefabScene( pfile );
// Assert initial state
assertInitialState( prefabScene );
// Create a new scene and instantiate the prefab
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone( Vector3.Zero );
// Override the property in the instance
setOverrideAction( instance );
// Assert that the override was set
assertOverrideSet( instance );
// Simulate the editor updating the patch after modification
instance.PrefabInstance.RefreshPatch();
// Ensure that when updating from the prefab the override is preserved
instance.UpdateFromPrefab();
// Assert that the override was preserved
assertOverridePreserved( instance );
}
[TestMethod]
public void OverriddenEnabledStateOnPrefabInstance()
{
TestOverriddenPropertyOnPrefabInstance(
"___prefab_enabled_override.prefab",
_basicPrefabSource,
prefab => Assert.IsTrue( prefab.Enabled, "Prefab should be enabled initially." ),
instance => instance.Enabled = false,
instance => Assert.IsFalse( instance.Enabled, "Prefab instance Enabled should be overridden to false." ),
instance => Assert.IsFalse( instance.Enabled, "Prefab instance override should be preserved after UpdateFromPrefab." )
);
}
[TestMethod]
public void OverriddenLocalTransformOnPrefabInstance()
{
var newTransform = new Transform( new Vector3( 1, 2, 3 ), Rotation.FromYaw( 45 ), new Vector3( 1, 2, 3 ) );
TestOverriddenPropertyOnPrefabInstance(
"___prefab_localtransform_override.prefab",
_basicPrefabSource,
prefab => Assert.AreEqual( new Transform( new Vector3( 788.8395f, -1793.604f, -1218.092f ), Rotation.Identity, new Vector3( 10, 10, 10 ) ), prefab.LocalTransform, "Prefab should have default LocalTransform." ),
instance => instance.LocalTransform = newTransform,
instance => Assert.AreEqual( newTransform, instance.LocalTransform, "Prefab instance LocalTransform should be overridden to new value." ),
instance => Assert.AreEqual( newTransform, instance.LocalTransform, "Prefab instance override should be preserved after UpdateFromPrefab." )
);
}
[TestMethod]
public void OverriddenTagsOnPrefabInstance()
{
TestOverriddenPropertyOnPrefabInstance(
"___prefab_tags_override.prefab",
_basicPrefabSource,
prefab => Assert.IsFalse( prefab.Tags.Has( "TestTag" ), "Prefab should not contain 'TestTag' initially." ),
instance => instance.Tags.Add( "TestTag" ),
instance => Assert.IsTrue( instance.Tags.Has( "TestTag" ), "Prefab instance should contain 'TestTag' after override." ),
instance => Assert.IsTrue( instance.Tags.Has( "TestTag" ), "Prefab instance override of Tags should be preserved after UpdateFromPrefab." )
);
}
[TestMethod]
public void RevertInstanceToPrefab_DoesNotRevertTransformNameFlagsOnRoot()
{
var saveLocation = "___revert_instance_test.prefab";
// Create our base prefab
using var prefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( saveLocation, _basicPrefabSource );
var pfile = ResourceLibrary.Get<PrefabFile>( saveLocation );
var prefabScene = SceneUtility.GetPrefabScene( pfile );
// Verify initial prefab state
Assert.AreEqual( "Object", prefabScene.Name );
Assert.AreEqual( new Transform( new Vector3( 788.8395f, -1793.604f, -1218.092f ), Rotation.Identity, new Vector3( 10, 10, 10 ) ), prefabScene.LocalTransform );
Assert.IsTrue( prefabScene.Enabled );
// Create a scene and instantiate the prefab
var scene = new Scene();
using var sceneScope = scene.Push();
var instance = prefabScene.Clone( Vector3.Zero );
// Modify instance's transform, name, and flags
instance.LocalTransform = new Transform( new Vector3( 100, 200, 300 ), Rotation.FromYaw( 45 ), Vector3.One );
instance.Name = "ModifiedInstance";
instance.Enabled = false;
instance.Flags = GameObjectFlags.DontDestroyOnLoad;
// Modify a component property
instance.Components.Get<ModelRenderer>( true ).Tint = Color.Blue;
// The editor would run this after a modification
instance.PrefabInstance.RefreshPatch();
// Revert instance to prefab
EditorUtility.Prefabs.RevertInstanceToPrefab( instance );
// Assert that transform, name, and flags are not reverted
Assert.AreEqual( new Transform( new Vector3( 100, 200, 300 ), Rotation.FromYaw( 45 ), Vector3.One ), instance.LocalTransform );
Assert.AreEqual( "ModifiedInstance", instance.Name );
Assert.IsFalse( instance.Enabled );
Assert.IsTrue( instance.Flags.HasFlag( GameObjectFlags.DontDestroyOnLoad ) );
// Assert that component overrides are reverted
Assert.AreEqual( new Color( 1, 0, 0, 1 ), instance.Components.Get<ModelRenderer>( true ).Tint );
}
[TestMethod]
public void NestedPrefabInstanceRefreshDeserialization()
{
// Setup prefab file paths
var nestedNestedPrefabLocation = "__nested_nested.prefab";
var nestedPrefabLocation = "__nested.prefab";
var basePrefabLocation = "__base.prefab";
// Step 1: Create the innermost prefab
using var nestedNestedPrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( nestedNestedPrefabLocation, _nestedNestedPrefabSource );
var nestedNestedPrefabFile = ResourceLibrary.Get<PrefabFile>( nestedNestedPrefabLocation );
var nestedNestedPrefabScene = SceneUtility.GetPrefabScene( nestedNestedPrefabFile );
// Step 2: Create the middle prefab that contains an instance of the innermost prefab
using var nestedPrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( nestedPrefabLocation, _nestedPrefabSource );
var nestedPrefabFile = ResourceLibrary.Get<PrefabFile>( nestedPrefabLocation );
var nestedPrefabScene = SceneUtility.GetPrefabScene( nestedPrefabFile );
// Step 3: Create the outermost prefab that contains an instance of the middle prefab
using var basePrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( basePrefabLocation, _basePrefabSource );
var basePrefabFile = ResourceLibrary.Get<PrefabFile>( basePrefabLocation );
var basePrefabScene = SceneUtility.GetPrefabScene( basePrefabFile );
// Step 4: Create a test scene and instantiate the outermost prefab
var scene = new Scene();
using var sceneScope = scene.Push();
var baseInstance = basePrefabScene.Clone( Vector3.Zero );
// Step 5: Verify the initial structure is correct
Assert.IsTrue( baseInstance.IsPrefabInstanceRoot );
Assert.AreEqual( "BasePrefab", baseInstance.Name );
Assert.IsNotNull( baseInstance.Components.Get<ModelRenderer>() );
Assert.AreEqual( 1, baseInstance.Children.Count );
var nestedInstanceInScene = baseInstance.Children[0];
Assert.IsTrue( nestedInstanceInScene.IsPrefabInstanceRoot );
Assert.AreEqual( "NestedPrefab", nestedInstanceInScene.Name );
Assert.IsNotNull( nestedInstanceInScene.Components.Get<BoxCollider>() );
Assert.AreEqual( 1, nestedInstanceInScene.Children.Count );
var nestedNestedInstanceInScene = nestedInstanceInScene.Children[0];
Assert.IsTrue( nestedNestedInstanceInScene.IsPrefabInstanceRoot );
Assert.AreEqual( "NestedNestedPrefab", nestedNestedInstanceInScene.Name );
Assert.IsNotNull( nestedNestedInstanceInScene.Components.Get<NavMeshArea>() );
// Step 6: Serialize and then deserialize the nested_nested instance
var serializedData = baseInstance.Serialize();
// Store some IDs for comparison after refresh
var originalId = nestedNestedInstanceInScene.Id;
var originalComponentId = nestedNestedInstanceInScene.Components.Get<NavMeshArea>().Id;
var originalPrefabSource = nestedNestedInstanceInScene.PrefabInstance.PrefabSource;
// Refresh through deserialization
baseInstance.Deserialize( serializedData, new GameObject.DeserializeOptions { IsRefreshing = true } );
// Step 7: Assert that the instance maintains proper identity and prefab relationships
Assert.AreEqual( originalId, nestedNestedInstanceInScene.Id, "GameObject ID should be preserved after refresh" );
Assert.AreEqual( originalComponentId, nestedNestedInstanceInScene.Components.Get<NavMeshArea>().Id, "Component ID should be preserved after refresh" );
// Verify the prefab instance relationships are still intact
Assert.IsTrue( nestedNestedInstanceInScene.IsPrefabInstanceRoot, "Should still be a prefab instance root after refresh" );
Assert.IsTrue( nestedNestedInstanceInScene.IsNestedPrefabInstanceRoot, "Should still be a nested prefab instance root after refresh" );
Assert.IsNotNull( nestedNestedInstanceInScene.PrefabInstance, "Should still have prefab instance data after refresh" );
Assert.AreEqual( originalPrefabSource, nestedNestedInstanceInScene.PrefabInstance.PrefabSource, "Prefab source should be preserved" );
// Verify the full hierarchy is still intact and properly connected
Assert.AreEqual( nestedInstanceInScene, nestedNestedInstanceInScene.Parent, "Parent relationship should be preserved" );
Assert.IsTrue( nestedInstanceInScene.IsPrefabInstanceRoot, "Should still be a prefab instance root after refresh" );
Assert.IsTrue( nestedInstanceInScene.IsNestedPrefabInstanceRoot, "Should still be a nested prefab instance root after refresh" );
Assert.IsTrue( nestedInstanceInScene.Children.Contains( nestedNestedInstanceInScene ), "Child relationship should be preserved" );
// Verify the outermost prefab instance root reference is still correct
Assert.AreEqual( baseInstance, nestedNestedInstanceInScene.OutermostPrefabInstanceRoot, "Outermost prefab root reference should be correct" );
}
[TestMethod]
public void BreakFromPrefabConvertsNestedInstancesToFullPrefabInstances()
{
// 1. Create the nested prefab (innermost)
var innerPrefabLocation = "__nestedPrefab.prefab";
using var innerPrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( innerPrefabLocation, _basicPrefabSource );
var innerPrefabFile = ResourceLibrary.Get<PrefabFile>( innerPrefabLocation );
var innerPrefabScene = SceneUtility.GetPrefabScene( innerPrefabFile );
// 2. Create the outer prefab that contains an instance of the nested prefab
var outerPrefabLocation = "__outer_for_break.prefab";
using var outerPrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( outerPrefabLocation, _outerPrefabWithNestedPrefabSource );
var outerPrefabFile = ResourceLibrary.Get<PrefabFile>( outerPrefabLocation );
var outerPrefabScene = SceneUtility.GetPrefabScene( outerPrefabFile );
// 3. Create a scene and instantiate the outer prefab
var scene = new Scene();
using var sceneScope = scene.Push();
var outerInstance = outerPrefabScene.Clone( Vector3.Zero );
// Verify initial state
Assert.IsTrue( outerInstance.IsPrefabInstanceRoot, "Outer instance should be a prefab instance root" );
Assert.AreEqual( 1, outerInstance.Children.Count, "Outer instance should have one child" );
var nestedInstance = outerInstance.Children[0];
Assert.IsTrue( nestedInstance.IsPrefabInstanceRoot, "Nested instance should be a prefab instance root" );
Assert.IsTrue( nestedInstance.IsNestedPrefabInstanceRoot, "Nested instance should be a nested prefab instance root" );
Assert.IsFalse( nestedInstance.IsOutermostPrefabInstanceRoot, "Nested instance should not be an outermost prefab instance root" );
Assert.AreEqual( outerInstance, nestedInstance.OutermostPrefabInstanceRoot, "Outer instance should be the outermost prefab instance root" );
// 4. Break the outer instance from its prefab
outerInstance.BreakFromPrefab();
// 5. Verify that outer instance is no longer a prefab instance
Assert.IsFalse( outerInstance.IsPrefabInstanceRoot, "Outer instance should no longer be a prefab instance root after breaking" );
Assert.IsFalse( outerInstance.IsPrefabInstance, "Outer instance should no longer be a prefab instance after breaking" );
// 6. Verify that nested instance is still a prefab instance but now a full instance
Assert.IsTrue( nestedInstance.IsPrefabInstanceRoot, "Nested instance should still be a prefab instance root after breaking parent" );
Assert.IsTrue( nestedInstance.IsPrefabInstance, "Nested instance should still be a prefab instance after breaking parent" );
Assert.IsFalse( nestedInstance.IsNestedPrefabInstanceRoot, "Nested instance should no longer be a nested prefab instance root" );
Assert.IsTrue( nestedInstance.IsOutermostPrefabInstanceRoot, "Nested instance should now be an outermost prefab instance root" );
Assert.AreEqual( nestedInstance, nestedInstance.OutermostPrefabInstanceRoot, "Nested instance should be its own outermost prefab instance root" );
// 7. Verify that the nested instance can still be updated from its prefab
// First, modify the original prefab by adding a new component
innerPrefabScene.AddComponent<BoxCollider>();
innerPrefabScene.ToPrefabFile();
// Update the nested instance and verify it received the changes
nestedInstance.UpdateFromPrefab();
Assert.IsTrue( nestedInstance.Components.Get<BoxCollider>() is not null,
"Nested instance should receive updates from its prefab after parent instance broke from prefab" );
}
// https://github.com/Facepunch/sbox-issues/issues/9194
[TestMethod]
public void WriteGameObjectToPrefab_WithNestedInstance()
{
// 1. Setup base prefab
var basePrefabLocation = "__writeback_nested_base.prefab";
using var basePrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( basePrefabLocation, _basicPrefabSource );
var basePrefabFile = ResourceLibrary.Get<PrefabFile>( basePrefabLocation );
var basePrefabScene = SceneUtility.GetPrefabScene( basePrefabFile );
// 2. Create another prefab to use as nested
var nestedPrefabLocation = "__writeback_nested_inner.prefab";
string nestedPrefabSource = _basicPrefabSource.Replace( "1,0,0,1", "0,1,0,1" ); // Change color to green
using var nestedPrefab = Sandbox.SceneTests.Helpers.RegisterPrefabFromJson( nestedPrefabLocation, nestedPrefabSource );
var nestedPrefabFile = ResourceLibrary.Get<PrefabFile>( nestedPrefabLocation );
var nestedPrefabScene = SceneUtility.GetPrefabScene( nestedPrefabFile );
// 3. Create a scene and instantiate the base prefab
var scene = new Scene();
using var sceneScope = scene.Push();
var baseInstance = basePrefabScene.Clone( Vector3.Zero );
// Verify initial state
Assert.IsTrue( baseInstance.IsPrefabInstanceRoot );
Assert.AreEqual( 0, baseInstance.Children.Count );
// 4. Add a nested prefab instance to the base instance
var nestedInstance = nestedPrefabScene.Clone( Vector3.Zero );
nestedInstance.SetParent( baseInstance );
var nestedOriginalId = nestedInstance.Id;
// The editor would call this after modification
baseInstance.PrefabInstance.RefreshPatch();
// Verify the nested instance was added
Assert.AreEqual( 1, baseInstance.Children.Count );
Assert.IsTrue( baseInstance.Children[0].IsPrefabInstanceRoot );
Assert.IsTrue( baseInstance.PrefabInstance.IsAddedGameObject( nestedInstance ) );
Assert.IsTrue( baseInstance.PrefabInstance.IsModified() );
// 5. Write the modified instance back to its prefab
EditorUtility.Prefabs.WriteInstanceToPrefab( baseInstance, true );
// Need to call this manually since we skip file write
EditorScene.UpdatePrefabInstances( basePrefabFile );
// 6. Verify that the prefab now contains the nested instance
basePrefabScene = SceneUtility.GetPrefabScene( basePrefabFile );
Assert.AreEqual( 1, basePrefabScene.Children.Count );
Assert.IsTrue( basePrefabScene.Children[0].IsPrefabInstanceRoot );
// 7. Verify that the instance patch is now empty since it matches the prefab
Assert.IsFalse( baseInstance.PrefabInstance.IsModified() );
Assert.IsFalse( baseInstance.PrefabInstance.IsAddedGameObject( nestedInstance ) );
// 8. Verify id mapping is intact
Assert.IsTrue( baseInstance.PrefabInstance.InstanceToPrefabLookup.ContainsKey( nestedOriginalId ) );
}
// Innermost prefab definition
static readonly string _nestedNestedPrefabSource = """"
{
"__guid": "1e26fa20-684e-44af-8a52-30df3a45ea28",
"Name": "NestedNestedPrefab",
"Position": "0,0,0",
"Scale": "1,1,1",
"Enabled": true,
"Components": [
{
"__type": "NavMeshArea",
"__guid": "ccefdfc7-aa08-444d-b5db-b2433aeb235e",
"Bounds": "64,64,64",
"Enabled": true
}
],
"Children": []
}
"""";
// Middle prefab definition with instance of innermost prefab
static readonly string _nestedPrefabSource = """"
{
"__guid": "def12345-6789-0abc-def1-23456789abcd",
"Name": "NestedPrefab",
"Position": "0,0,0",
"Scale": "1,1,1",
"Enabled": true,
"Components": [
{
"__type": "BoxCollider",
"__guid": "98765432-fedc-ba98-7654-321012345678",
"Size": "50,50,50",
"IsTrigger": false,
"Enabled": true
}
],
"Children": [
{
"__guid": "4515d718-070a-4de3-bbda-9835c6e118c9",
"__version": 1,
"__Prefab": "__nested_nested.prefab",
"__PrefabInstancePatch": {
"AddedObjects": [],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"1e26fa20-684e-44af-8a52-30df3a45ea28": "4515d718-070a-4de3-bbda-9835c6e118c9",
"ccefdfc7-aa08-444d-b5db-b2433aeb235e": "3bfd4ead-0d39-4ad1-8707-e45f47045776"
}
}
]
}
"""";
// Outermost prefab definition with instance of middle prefab
static readonly string _basePrefabSource = """"
{
"__guid": "01234567-89ab-cdef-0123-456789abcdef",
"Name": "BasePrefab",
"Position": "0,0,0",
"Scale": "1,1,1",
"Enabled": true,
"Components": [
{
"__type": "ModelRenderer",
"__guid": "fedcba98-7654-3210-fedc-ba9876543210",
"BodyGroups": 18446744073709551615,
"MaterialGroup": null,
"MaterialOverride": null,
"Model": null,
"RenderType": "On",
"Tint": "1,0,0,1"
}
],
"Children": [
{
"__guid": "2fe84d07-0e73-48f7-a8c8-e6d90c58082d",
"__version": 1,
"__Prefab": "__nested.prefab",
"__PrefabInstancePatch": {
"AddedObjects": [],
"RemovedObjects": [],
"PropertyOverrides": [],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"def12345-6789-0abc-def1-23456789abcd": "2fe84d07-0e73-48f7-a8c8-e6d90c58082d",
"4515d718-070a-4de3-bbda-9835c6e118c9": "a5c72647-dc84-4557-b8fd-112e8b16b3b5",
"3bfd4ead-0d39-4ad1-8707-e45f47045776": "34567890-abcd-ef12-3456-789012345678",
"98765432-fedc-ba98-7654-321012345678": "abcdef01-2345-6789-abcd-ef0123456789"
}
}
]
}
"""";
}