Spawner/DuplicatorSpawner.cs

Spawner for duplicator contraptions. It holds duplication data and serialized payload, ensures required cloud packages are installed, draws a preview of the dupe models, and can spawn game objects into the scene by deserializing the stored duplication JSON and instantiating GameObject instances with ownership and network spawn.

NetworkingExternal Download
using System.Text.Json;
using System.Text.Json.Nodes;

/// <summary>
/// Payload for spawning a duplicator contraption.
/// </summary>
public class DuplicatorSpawner : ISpawner
{
	public string DisplayName { get; private set; } = "Duplication";
	public string Icon { get; init; }
	public BBox Bounds => Dupe?.Bounds ?? default;
	public bool IsReady => Dupe is not null && _packagesReady;
	public Task<bool> Loading { get; }

	public string Data => Sandbox.Json.Serialize( new DupeInfo( Icon, Json ) );

	public DuplicationData Dupe { get; }

	public string Json { get; }

	private bool _packagesReady;

	public DuplicatorSpawner( DuplicationData dupe, string json, string name = null, string icon = null )
	{
		Dupe = dupe;
		Json = json;
		Icon = icon;
		DisplayName = name ?? "Duplication";
		Loading = InstallPackages();
	}

	/// <summary>
	/// Create from raw dupe JSON (e.g. from a storage entry). No icon.
	/// </summary>
	public static DuplicatorSpawner FromJson( string json, string name = null, string icon = null )
	{
		var dupe = Sandbox.Json.Deserialize<DuplicationData>( json );
		return new DuplicatorSpawner( dupe, json, name, icon );
	}

	/// <summary>
	/// Creates a duplicator spawner from the serialized data string. This is what gets synced to clients, so it includes the icon and raw JSON.
	/// </summary>
	/// <param name="data"></param>
	/// <returns></returns>
	public static DuplicatorSpawner FromData( string data )
	{
		var payload = Sandbox.Json.Deserialize<DupeInfo>( data );
		var dupe = Sandbox.Json.Deserialize<DuplicationData>( payload.Json );
		return new DuplicatorSpawner( dupe, payload.Json, icon: payload.Icon );
	}

	private record DupeInfo( string Icon, string Json );

	private async Task<bool> InstallPackages()
	{
		if ( Dupe?.Packages is null || Dupe.Packages.Count == 0 )
		{
			_packagesReady = true;
			return true;
		}

		foreach ( var pkg in Dupe.Packages )
		{
			if ( Cloud.IsInstalled( pkg ) )
				continue;

			await Cloud.Load( pkg );
		}

		_packagesReady = true;
		return true;
	}

	public void DrawPreview( Transform transform, Material overrideMaterial )
	{
		if ( Dupe is null ) return;

		foreach ( var model in Dupe.PreviewModels )
		{
			if ( model.Model is null )
				continue;

			if ( model.Model.IsError )
			{
				var bounds = model.Bounds;
				if ( bounds.Size.IsNearlyZero() ) continue;

				var t = transform.ToWorld( model.Transform );
				t = new Transform( t.PointToWorld( bounds.Center ), t.Rotation, t.Scale * (bounds.Size / 50) );
				Game.ActiveScene.DebugOverlay.Model( Model.Cube, transform: t, overlay: false, materialOveride: overrideMaterial );
			}
			else
			{
				Game.ActiveScene.DebugOverlay.Model( model.Model, transform: transform.ToWorld( model.Transform ), overlay: false, materialOveride: overrideMaterial, localBoneTransforms: model.Bones );
			}
		}
	}

	public Task<List<GameObject>> Spawn( Transform transform, Player player )
	{
		var jsonObject = Sandbox.Json.ToNode( Dupe ) as JsonObject;
		SceneUtility.MakeIdGuidsUnique( jsonObject );

		var results = new List<GameObject>();

		using ( Game.ActiveScene.BatchGroup() )
		{
			foreach ( var entry in jsonObject["Objects"] as JsonArray )
			{
				if ( entry is not JsonObject obj )
					continue;

				var pos = entry["Position"]?.Deserialize<Vector3>() ?? default;
				var rot = entry["Rotation"]?.Deserialize<Rotation>() ?? Rotation.Identity;
				var scl = entry["Scale"]?.Deserialize<Vector3>() ?? Vector3.One;

				var world = transform.ToWorld( new Transform( pos, rot ) );
				world.Scale = scl;

				var go = new GameObject( false );
				go.Deserialize( obj, new GameObject.DeserializeOptions { TransformOverride = world } );

				Ownable.Set( go, player.Network.Owner );
				go.NetworkSpawn( true, null );

				results.Add( go );
			}
		}

		return Task.FromResult( results );
	}
}