terminalCode Example

Sandbox: Duplicator tool — copy, paste, and save contraptions with DuplicationData

calendar_today May 4, 2026 schedule ~1 min read person patrickjr verified 50

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

Was this helpful?