terminalCode Example
Sandbox: Duplicator tool — copy, paste, and save contraptions with DuplicationData
Duplicator Tool — Copy, Paste, and Save Contraptions
The Duplicator tool copies a contraption (a connected graph of GameObjects) to JSON and pastes it back. It uses LinkedGameObjectBuilder to walk the contraption graph and DuplicationData to serialize it.
Copy flow
CSHARP
[Rpc.Host]
public void Copy( GameObject obj, Transform selectionAngle, bool additive )
{
if ( !additive )
builder.Clear();
// Walk the contraption graph from the selected object
builder.AddConnected( obj );
builder.RemoveDeletedObjects();
// Serialize to JSON (stored as a [Sync] string)
var tempDupe = DuplicationData.CreateFromObjects( builder.Objects, selectionAngle );
CopiedJson = Json.Serialize( tempDupe );
PlayerData.For( Rpc.Caller )?.AddStat( "tool.duplicator.copy" );
}Paste flow
CSHARP
// Right-click to copy, left-click to paste
public override void OnControl()
{
if ( Input.Pressed( "attack1" ) && spawner is not null )
{
var select = TraceSelect();
if ( !select.IsValid() ) return;
var tx = new Transform();
tx.Position = select.WorldPosition() + Vector3.Down * spawner.Bounds.Mins.z;
// Align yaw to player's facing direction + accumulated rotation offset
var relative = Player.EyeTransform.Rotation.Angles();
tx.Rotation = Rotation.From( new Angles( 0, relative.yaw, 0 ) ) * _rotationOffset;
Duplicate( tx );
ShootEffects( select );
_rotationOffset = Rotation.Identity;
}
if ( Input.Pressed( "attack2" ) )
{
var selectionAngle = new Transform( select.WorldPosition(),
Player.EyeTransform.Rotation.Angles().WithPitch( 0 ) );
Copy( select.GameObject, selectionAngle, Input.Down( "run" ) ); // Hold run = additive copy
}
}DuplicationData structure
CSHARP
public class DuplicationData
{
// Serialized JSON of all GameObjects in the contraption
public List<JsonObject> Objects { get; set; }
// Bounding box in selection space — used to place the dupe on surfaces
public BBox Bounds { get; set; }
// Preview models for the ghost overlay
public List<PreviewModel> PreviewModels { get; set; }
public record struct PreviewModel( Model Model, Transform Transform, Transform[] Bones, BBox Bounds );
}Save to storage / load from workshop
CSHARP
// Save current dupe to local storage
public void Save()
{
string data = CopiedJson;
var packages = Cloud.ResolvePrimaryAssetsFromJson( data );
var storage = Storage.CreateEntry( "dupe" );
storage.SetMeta( "packages", packages.Select( x => x.FullIdent ) );
storage.Files.WriteAllText( "/dupe.json", data );
}
// Load from local storage
public static void FromStorage( Storage.Entry item )
{
var localPlayer = Player.FindLocalPlayer();
var inventory = localPlayer.GetComponent<PlayerInventory>();
inventory.SetToolMode( "Duplicator" );
var toolmode = localPlayer.GetComponentInChildren<Duplicator>( true );
var json = item.Files.ReadAllText( "/dupe.json" );
toolmode.Load( json );
}
// Install from workshop then load
public static async Task FromWorkshop( Storage.QueryItem item )
{
var installed = await item.Install();
if ( installed != null ) FromStorage( installed );
}Key points
- CopiedJson is [Sync( SyncFlags.FromHost )] — all clients see the current dupe, enabling live previews of other players' pastes
- LinkedGameObjectBuilder.RejectPlayers = true prevents accidentally copying players
- Hold run (Shift) while right-clicking to add to an existing selection (additive copy)
- The preview ghost is drawn in OnUpdate on every client — not just the owner
- Cloud.ResolvePrimaryAssetsFromJson extracts package idents from the JSON so dependencies can be listed in the save metadata
Was this helpful?