mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 00:08:05 -04:00
166 lines
5.2 KiB
C#
166 lines
5.2 KiB
C#
using System.Linq;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace SceneTests.GameObjects;
|
|
|
|
/// <summary>
|
|
/// Pins the blob data contract the scene editor undo/redo snapshots rely on:
|
|
/// <see cref="GameObject.Deserialize"/> defers component PostDeserialize to the
|
|
/// <see cref="CallbackBatch"/> flush, so any blob context supplying that data (eg. a
|
|
/// MeshComponent's PolygonMesh) must outlive the batch. A per-object context disposed
|
|
/// before the flush would leave the mesh empty - the "create block, undo, redo,
|
|
/// block isn't there" bug.
|
|
/// </summary>
|
|
[TestClass]
|
|
[DoNotParallelize]
|
|
public class SerializeBlobDataTest
|
|
{
|
|
static GameObject CreateMeshObject()
|
|
{
|
|
var go = new GameObject( true, "Block" );
|
|
var component = go.Components.Create<MeshComponent>();
|
|
|
|
var mesh = new PolygonMesh();
|
|
var a = mesh.AddVertex( new Vector3( 0, 0, 0 ) );
|
|
var b = mesh.AddVertex( new Vector3( 64, 0, 0 ) );
|
|
var c = mesh.AddVertex( new Vector3( 64, 64, 0 ) );
|
|
var d = mesh.AddVertex( new Vector3( 0, 64, 0 ) );
|
|
mesh.AddFace( a, b, c, d );
|
|
|
|
component.Mesh = mesh;
|
|
return go;
|
|
}
|
|
|
|
static JsonObject CaptureSnapshot( GameObject go )
|
|
{
|
|
// Same shape as SceneUndoSnapshot.GameObjectSnapshot capture
|
|
using var blobs = BlobDataSerializer.Capture();
|
|
var json = go.Serialize();
|
|
blobs.SaveTo( json );
|
|
return json;
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MeshSerializesAsBlobReference()
|
|
{
|
|
var scene = new Scene();
|
|
using var sceneScope = scene.Push();
|
|
|
|
var state = CaptureSnapshot( CreateMeshObject() );
|
|
var json = state.ToJsonString();
|
|
|
|
Assert.IsTrue( json.Contains( "$blob" ), "Mesh should serialize as a blob reference under an active capture context" );
|
|
Assert.IsTrue( state.ContainsKey( "__blobdata" ), "SaveTo should attach the blob data to the snapshot json" );
|
|
}
|
|
|
|
[TestMethod]
|
|
public void UnchangedMeshSerializesIdentically()
|
|
{
|
|
var scene = new Scene();
|
|
using var sceneScope = scene.Push();
|
|
|
|
var go = CreateMeshObject();
|
|
|
|
// Serializing an unchanged mesh twice must produce byte-identical output ($blob id
|
|
// and blob data), otherwise every scene save changes the file checksum and
|
|
// triggers pointless recompiles.
|
|
var state1 = CaptureSnapshot( go );
|
|
var state2 = CaptureSnapshot( go );
|
|
|
|
Assert.AreEqual( state1.ToJsonString(), state2.ToJsonString(), "Re-saving an unchanged mesh must produce identical output" );
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MeshSurvivesDeferredDeserialize()
|
|
{
|
|
var scene = new Scene();
|
|
using var sceneScope = scene.Push();
|
|
|
|
var go = CreateMeshObject();
|
|
var state = CaptureSnapshot( go );
|
|
go.DestroyImmediate();
|
|
|
|
// Same shape as the undo/redo lambdas: the shared blob context is declared before
|
|
// the batch, so it's still open when the flush runs the deferred PostDeserialize.
|
|
GameObject restored;
|
|
using ( var blobs = BlobDataSerializer.LoadFromMemory( default ) )
|
|
using ( CallbackBatch.Batch() )
|
|
{
|
|
restored = new GameObject();
|
|
restored.DeserializeId( state );
|
|
|
|
blobs.LoadFrom( state );
|
|
restored.Deserialize( state, new GameObject.DeserializeOptions { IsRefreshing = true } );
|
|
}
|
|
|
|
var component = restored.Components.Get<MeshComponent>();
|
|
Assert.IsNotNull( component );
|
|
Assert.IsNotNull( component.Mesh, "Mesh should deserialize from blob data after the batch flush" );
|
|
Assert.AreEqual( 4, component.Mesh.VertexHandles.Count() );
|
|
Assert.AreEqual( 1, component.Mesh.FaceHandles.Count() );
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MultipleSnapshotsMergeIntoSharedContext()
|
|
{
|
|
var scene = new Scene();
|
|
using var sceneScope = scene.Push();
|
|
|
|
// Each object is captured in its own context, like the snapshot loop does
|
|
var go1 = CreateMeshObject();
|
|
var go2 = CreateMeshObject();
|
|
var state1 = CaptureSnapshot( go1 );
|
|
var state2 = CaptureSnapshot( go2 );
|
|
go1.DestroyImmediate();
|
|
go2.DestroyImmediate();
|
|
|
|
GameObject restored1, restored2;
|
|
using ( var blobs = BlobDataSerializer.LoadFromMemory( default ) )
|
|
using ( CallbackBatch.Batch() )
|
|
{
|
|
restored1 = new GameObject();
|
|
restored1.DeserializeId( state1 );
|
|
restored2 = new GameObject();
|
|
restored2.DeserializeId( state2 );
|
|
|
|
blobs.LoadFrom( state1 );
|
|
restored1.Deserialize( state1, new GameObject.DeserializeOptions { IsRefreshing = true } );
|
|
blobs.LoadFrom( state2 );
|
|
restored2.Deserialize( state2, new GameObject.DeserializeOptions { IsRefreshing = true } );
|
|
}
|
|
|
|
Assert.AreEqual( 4, restored1.Components.Get<MeshComponent>().Mesh?.VertexHandles.Count() );
|
|
Assert.AreEqual( 4, restored2.Components.Get<MeshComponent>().Mesh?.VertexHandles.Count() );
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ComponentSnapshotPathRestoresMesh()
|
|
{
|
|
var scene = new Scene();
|
|
using var sceneScope = scene.Push();
|
|
|
|
var go = CreateMeshObject();
|
|
var component = go.Components.Get<MeshComponent>();
|
|
|
|
// Same shape as SceneUndoSnapshot.ComponentSnapshot: capture, then restore with a
|
|
// per-item context - valid because PostDeserialize is called synchronously inside it.
|
|
JsonNode state;
|
|
using ( var blobs = BlobDataSerializer.Capture() )
|
|
{
|
|
state = component.Serialize( new GameObject.SerializeOptions() );
|
|
blobs.SaveTo( state );
|
|
}
|
|
|
|
component.Mesh = null;
|
|
|
|
component.Deserialize( state.AsObject() );
|
|
using ( BlobDataSerializer.LoadFrom( state ) )
|
|
{
|
|
component.PostDeserialize();
|
|
}
|
|
|
|
Assert.IsNotNull( component.Mesh );
|
|
Assert.AreEqual( 4, component.Mesh.VertexHandles.Count() );
|
|
}
|
|
}
|