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

490 lines
17 KiB
C#

using Sandbox.Internal;
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace GameObjects;
[TestClass]
public class Refresh
{
TypeLibrary TypeLibrary;
private TypeLibrary _oldTypeLibrary;
[TestInitialize]
public void TestInitialize()
{
_oldTypeLibrary = Game.TypeLibrary;
TypeLibrary = new Sandbox.Internal.TypeLibrary();
TypeLibrary.AddAssembly( typeof( ModelRenderer ).Assembly, false );
TypeLibrary.AddAssembly( typeof( ComponentIdTest ).Assembly, false );
Game.TypeLibrary = TypeLibrary;
}
[TestCleanup]
public void Cleanup()
{
Game.TypeLibrary = _oldTypeLibrary;
}
[TestMethod]
public void RegularRefreshPrunesAllMissingObjects()
{
using var scope = new Scene().Push();
// Create a hierarchy with parent and children
var parent = new GameObject( true, "Parent" );
// Child objects with various configurations
var child1 = new GameObject( parent, true, "Child1" );
var child2 = new GameObject( parent, true, "Child2" );
var child3 = new GameObject( parent, true, "Child3" );
// Serialize the parent to get the whole hierarchy
var originalJson = parent.Serialize();
// Remove child2 from the serialized data
var childrenArray = originalJson["Children"].AsArray();
JsonNode removedChild = null;
for ( int i = 0; i < childrenArray.Count; i++ )
{
var childJson = childrenArray[i].AsObject();
if ( childJson["Name"].GetValue<string>() == "Child2" )
{
removedChild = childrenArray[i];
childrenArray.RemoveAt( i );
break;
}
}
Assert.IsNotNull( removedChild, "Failed to find Child2 in JSON to remove" );
// Now deserialize with IsRefreshing=true, IsNetworkRefresh=false
var deserializeOptions = new GameObject.DeserializeOptions
{
IsRefreshing = true,
IsNetworkRefresh = false
};
parent.Deserialize( originalJson, deserializeOptions );
// Verify child1 and child3 still exist, but child2 was removed
Assert.AreEqual( 2, parent.Children.Count, "Parent should have 2 children after refresh" );
var remainingNames = parent.Children.Select( c => c.Name ).ToArray();
Assert.IsTrue( remainingNames.Contains( "Child1" ), "Child1 should still exist" );
Assert.IsFalse( remainingNames.Contains( "Child2" ), "Child2 should have been removed" );
Assert.IsTrue( remainingNames.Contains( "Child3" ), "Child3 should still exist" );
}
[TestMethod]
public void NetworkRefreshPreservesNonSnapshotObjects()
{
using var scope = new Scene().Push();
// Create a hierarchy with parent as a network object
var parent = new GameObject( true, "Parent" );
parent.NetworkMode = NetworkMode.Object;
// Child with NetworkMode.Snapshot - should be pruned if not in JSON
var snapshotChild = new GameObject( parent, true, "SnapshotChild" );
snapshotChild.NetworkMode = NetworkMode.Snapshot;
// Child with NetworkMode.Object - should be preserved
var networkObjectChild = new GameObject( parent, true, "NetworkObjectChild" );
networkObjectChild.NetworkMode = NetworkMode.Object;
// Child with NetworkMode.Snapshot but NotNetworked flag - should be preserved
var notNetworkedChild = new GameObject( parent, true, "NotNetworkedChild" );
notNetworkedChild.NetworkMode = NetworkMode.Snapshot;
notNetworkedChild.Flags |= GameObjectFlags.NotNetworked;
// Child with NetworkMode.Never - should be preserved
var neverNetworkedChild = new GameObject( parent, true, "NeverNetworkedChild" );
neverNetworkedChild.NetworkMode = NetworkMode.Never;
// Serialize the parent with SingleNetworkObject option
var serializeOptions = new GameObject.SerializeOptions
{
SingleNetworkObject = true
};
var originalJson = parent.Serialize( serializeOptions );
// Remove all children from the serialized data
var childrenArray = originalJson["Children"].AsArray();
childrenArray.Clear();
// Now deserialize with IsRefreshing=true and IsNetworkRefresh=true
var deserializeOptions = new GameObject.DeserializeOptions
{
IsRefreshing = true,
IsNetworkRefresh = true
};
parent.Deserialize( originalJson, deserializeOptions );
// Verify only Snapshot objects without NotNetworked flag are removed
Assert.AreEqual( 3, parent.Children.Count, "Parent should have 3 children after network refresh" );
var remainingNames = parent.Children.Select( c => c.Name ).ToArray();
Assert.IsFalse( remainingNames.Contains( "SnapshotChild" ), "SnapshotChild should have been removed" );
Assert.IsTrue( remainingNames.Contains( "NetworkObjectChild" ), "NetworkObjectChild should still exist" );
Assert.IsTrue( remainingNames.Contains( "NotNetworkedChild" ), "NotNetworkedChild should still exist" );
Assert.IsTrue( remainingNames.Contains( "NeverNetworkedChild" ), "NeverNetworkedChild should still exist" );
}
[TestMethod]
public void NetworkRefreshPrunesComponentsInRemainingObjects()
{
using var scope = new Scene().Push();
// Create a GameObject with NetworkMode.Object for network refresh
var go = new GameObject( true, "TestObject" );
go.NetworkMode = NetworkMode.Object;
// Add multiple components
var comp1 = go.Components.Create<ModelRenderer>();
var comp2 = go.Components.Create<ComponentIdTest>();
// Save component IDs for verification
var comp1Id = comp1.Id;
var comp2Id = comp2.Id;
// Serialize the object with SingleNetworkObject option
var serializeOptions = new GameObject.SerializeOptions
{
SingleNetworkObject = true
};
var originalJson = go.Serialize( serializeOptions );
// Remove comp2 from the serialized data
var componentsArray = originalJson["Components"].AsArray();
for ( int i = 0; i < componentsArray.Count; i++ )
{
var componentJson = componentsArray[i].AsObject();
if ( componentJson[Component.JsonKeys.Id].GetValue<Guid>() == comp2Id )
{
componentsArray.RemoveAt( i );
break;
}
}
// Deserialize with IsRefreshing=true and IsNetworkRefresh=true
var deserializeOptions = new GameObject.DeserializeOptions
{
IsRefreshing = true,
IsNetworkRefresh = true
};
go.Deserialize( originalJson, deserializeOptions );
// Verify that comp1 still exists but comp2 was removed
Assert.AreEqual( 1, go.Components.Count, "Should have only one component after refresh" );
var remainingComponents = go.Components.GetAll().ToList();
Assert.AreEqual( comp1Id, remainingComponents[0].Id, "comp1 should still exist" );
}
[TestMethod]
public void NetworkRefreshPreservesHierarchyWithNestedSnapshots()
{
using var scope = new Scene().Push();
// Create a hierarchy with network objects at different levels
var parent = new GameObject( true, "Parent" );
parent.NetworkMode = NetworkMode.Object;
// Child with NetworkMode.Snapshot - should be preserved if in JSON, pruned if not
var child1 = new GameObject( parent, true, "Child1" );
child1.NetworkMode = NetworkMode.Snapshot;
// Grandchild with NetworkMode.Snapshot - should be pruned if not in JSON
var grandchild1 = new GameObject( child1, true, "GrandChild1" );
grandchild1.NetworkMode = NetworkMode.Snapshot;
// Child with NetworkMode.Object - should be preserved
var child2 = new GameObject( parent, true, "Child2" );
child2.NetworkMode = NetworkMode.Object;
// Serialize the parent with SingleNetworkObject option
var serializeOptions = new GameObject.SerializeOptions
{
SingleNetworkObject = true
};
var originalJson = parent.Serialize( serializeOptions );
// Find Child1 in the JSON
var childrenArray = originalJson["Children"].AsArray();
JsonObject child1Json = FindChildByName( originalJson, "Child1" );
Assert.IsNotNull( child1Json, "Failed to find Child1 in JSON" );
// Remove GrandChild1 from Child1's children
var child1ChildrenArray = child1Json["Children"].AsArray();
child1ChildrenArray.Clear();
// Deserialize with IsRefreshing=true and IsNetworkRefresh=true
var deserializeOptions = new GameObject.DeserializeOptions
{
IsRefreshing = true,
IsNetworkRefresh = true
};
parent.Deserialize( originalJson, deserializeOptions );
// Verify child1 still exists (it was in the JSON)
var child1After = parent.Children.FirstOrDefault( c => c.Name == "Child1" );
Assert.IsNotNull( child1After, "Child1 should still exist" );
// Verify grandchild1 was pruned (it's NetworkMode.Snapshot and wasn't in the JSON)
Assert.AreEqual( 0, child1After.Children.Count, "Child1 should have no children after refresh" );
}
[TestMethod]
public void NetworkRefreshPreservesNotNetworkedComponents()
{
using var scope = new Scene().Push();
// Create a GameObject with NetworkMode.Object for network refresh
var go = new GameObject( true, "TestObject" );
go.NetworkMode = NetworkMode.Object;
// Add multiple components
var comp1 = go.Components.Create<ModelRenderer>();
var comp2 = go.Components.Create<ComponentIdTest>();
// Mark comp2 as not networked
comp2.Flags |= ComponentFlags.NotNetworked;
// Save component IDs for verification
var comp1Id = comp1.Id;
var comp2Id = comp2.Id;
// Serialize the object with SingleNetworkObject option
var serializeOptions = new GameObject.SerializeOptions
{
SingleNetworkObject = true
};
var originalJson = go.Serialize( serializeOptions );
// Remove all components from the serialized data
originalJson["Components"] = new JsonArray();
// Deserialize with IsRefreshing=true and IsNetworkRefresh=true
var deserializeOptions = new GameObject.DeserializeOptions
{
IsRefreshing = true,
IsNetworkRefresh = true
};
go.Deserialize( originalJson, deserializeOptions );
// Verify that comp1 was removed (it was networked and not in JSON)
// but comp2 was preserved (it had NotNetworked flag)
Assert.AreEqual( 1, go.Components.Count, "Should have only one component after refresh" );
var remainingComponents = go.Components.GetAll().ToList();
Assert.AreEqual( comp2Id, remainingComponents[0].Id, "NotNetworked component should be preserved" );
Assert.IsTrue( remainingComponents[0] is ComponentIdTest, "Remaining component should be ComponentIdTest" );
}
[TestMethod]
public void RegularRefreshAddsNewGameObjects()
{
using var scope = new Scene().Push();
// Create a hierarchy with parent
var parent = new GameObject( true, "Parent" );
// Serialize the parent to get the initial hierarchy
var originalJson = parent.Serialize();
// Add a new child to the JSON (not in the actual hierarchy)
var childrenArray = originalJson["Children"].AsArray();
var newChildJson = new JsonObject
{
["__guid"] = Guid.NewGuid(),
["Name"] = "NewChild",
["Position"] = JsonValue.Create( Vector3.Zero ),
["Rotation"] = JsonValue.Create( Rotation.Identity ),
["Scale"] = JsonValue.Create( Vector3.One ),
["Enabled"] = true
};
childrenArray.Add( newChildJson );
// Now deserialize with IsRefreshing=true
var deserializeOptions = new GameObject.DeserializeOptions
{
IsRefreshing = true
};
parent.Deserialize( originalJson, deserializeOptions );
// Verify the new child was added
Assert.AreEqual( 1, parent.Children.Count, "Parent should have 1 child after refresh" );
Assert.AreEqual( "NewChild", parent.Children[0].Name, "New child should have been added" );
}
[TestMethod]
public void RegularRefreshAddsNewComponents()
{
using var scope = new Scene().Push();
// Create a GameObject
var go = new GameObject( true, "TestObject" );
// Add an initial component
var comp1 = go.Components.Create<ModelRenderer>();
// Serialize the object
var originalJson = go.Serialize();
// Add a new component to the JSON
var componentsArray = originalJson["Components"].AsArray();
var newComponentJson = new JsonObject
{
["__guid"] = Guid.NewGuid(),
["__type"] = "ComponentIdTest",
["__enabled"] = true
};
componentsArray.Add( newComponentJson );
// Deserialize with IsRefreshing=true
var deserializeOptions = new GameObject.DeserializeOptions
{
IsRefreshing = true
};
go.Deserialize( originalJson, deserializeOptions );
// Verify both components exist
Assert.AreEqual( 2, go.Components.Count, "GameObject should have 2 components after refresh" );
Assert.IsNotNull( go.Components.Get<ModelRenderer>(), "Original component should still exist" );
Assert.IsNotNull( go.Components.Get<ComponentIdTest>(), "New component should have been added" );
}
[TestMethod]
public void RegularRefreshMaintainsHierarchyRelationships()
{
using var scope = new Scene().Push();
// Create a complex hierarchy
var parent = new GameObject( true, "Parent" );
var child1 = new GameObject( parent, true, "Child1" );
var grandChild1A = new GameObject( child1, true, "GrandChild1A" );
var grandChild1B = new GameObject( child1, true, "GrandChild1B" );
var child2 = new GameObject( parent, true, "Child2" );
var grandChild2A = new GameObject( child2, true, "GrandChild2A" );
// Keep track of original hierarchy relationships and IDs
var originalHierarchy = new Dictionary<string, List<string>>();
originalHierarchy["Parent"] = parent.Children.Select( c => c.Name ).ToList();
originalHierarchy["Child1"] = child1.Children.Select( c => c.Name ).ToList();
originalHierarchy["Child2"] = child2.Children.Select( c => c.Name ).ToList();
var originalIds = new Dictionary<string, Guid>();
originalIds["Parent"] = parent.Id;
originalIds["Child1"] = child1.Id;
originalIds["Child2"] = child2.Id;
originalIds["GrandChild1A"] = grandChild1A.Id;
originalIds["GrandChild1B"] = grandChild1B.Id;
originalIds["GrandChild2A"] = grandChild2A.Id;
// Serialize the parent
var originalJson = parent.Serialize();
// Modify the JSON: remove GrandChild1B and add a new GrandChild2B
var child1Json = FindChildByName( originalJson, "Child1" );
var child1Children = child1Json["Children"].AsArray();
// Remove GrandChild1B
for ( int i = 0; i < child1Children.Count; i++ )
{
if ( child1Children[i]["Name"].GetValue<string>() == "GrandChild1B" )
{
child1Children.RemoveAt( i );
break;
}
}
// Add GrandChild2B to Child2
var child2Json = FindChildByName( originalJson, "Child2" );
var child2Children = child2Json["Children"].AsArray();
var newGrandChildJson = new JsonObject
{
["__guid"] = Guid.NewGuid(),
["Name"] = "GrandChild2B",
["Position"] = JsonValue.Create( Vector3.Zero ),
["Rotation"] = JsonValue.Create( Rotation.Identity ),
["Scale"] = JsonValue.Create( Vector3.One ),
["Enabled"] = true
};
child2Children.Add( newGrandChildJson );
// Deserialize with IsRefreshing=true
var deserializeOptions = new GameObject.DeserializeOptions
{
IsRefreshing = true
};
parent.Deserialize( originalJson, deserializeOptions );
// Verify the hierarchy was updated correctly
Assert.AreEqual( 2, parent.Children.Count, "Parent should still have 2 children" );
var refreshedChild1 = parent.Children.FirstOrDefault( c => c.Name == "Child1" );
var refreshedChild2 = parent.Children.FirstOrDefault( c => c.Name == "Child2" );
Assert.IsNotNull( refreshedChild1, "Child1 should still exist" );
Assert.IsNotNull( refreshedChild2, "Child2 should still exist" );
// Child1 should have lost GrandChild1B
Assert.AreEqual( 1, refreshedChild1.Children.Count, "Child1 should have 1 child after refresh" );
Assert.AreEqual( "GrandChild1A", refreshedChild1.Children[0].Name, "GrandChild1A should still exist" );
// Child2 should have gained GrandChild2B
Assert.AreEqual( 2, refreshedChild2.Children.Count, "Child2 should have 2 children after refresh" );
var grandChildNames = refreshedChild2.Children.Select( c => c.Name ).ToArray();
Assert.IsTrue( grandChildNames.Contains( "GrandChild2A" ), "GrandChild2A should still exist" );
Assert.IsTrue( grandChildNames.Contains( "GrandChild2B" ), "GrandChild2B should have been added" );
// IDs should be preserved for existing objects
Assert.AreEqual( originalIds["Parent"], parent.Id, "Parent ID should be preserved" );
Assert.AreEqual( originalIds["Child1"], refreshedChild1.Id, "Child1 ID should be preserved" );
Assert.AreEqual( originalIds["Child2"], refreshedChild2.Id, "Child2 ID should be preserved" );
var refreshedGrandChild1A = refreshedChild1.Children.FirstOrDefault( c => c.Name == "GrandChild1A" );
Assert.AreEqual( originalIds["GrandChild1A"], refreshedGrandChild1A.Id, "GrandChild1A ID should be preserved" );
var refreshedGrandChild2A = refreshedChild2.Children.FirstOrDefault( c => c.Name == "GrandChild2A" );
Assert.AreEqual( originalIds["GrandChild2A"], refreshedGrandChild2A.Id, "GrandChild2A ID should be preserved" );
}
// Helper method to find a child object in the JSON by name
private JsonObject FindChildByName( JsonObject parentJson, string childName )
{
var childrenArray = parentJson["Children"].AsArray();
foreach ( var child in childrenArray )
{
if ( child["Name"].GetValue<string>() == childName )
{
return child.AsObject();
}
}
return null;
}
}