Weapons/ToolGun/Modes/Duplicator/DuplicationData.cs
using System.Text.Json.Nodes;
/// <summary>
/// Holds a bunch of GameObject json, a bounding box, and some preview models for a
/// duplication. This is what gets serialized to a string and stored in the Duplicator tool.
/// The objects and the bounds are created in selection space. Where the user right clicked to
/// select is 0,0,0, and the player's view yaw is the rotation identity.
/// </summary>
public class DuplicationData
{
/// <summary>
/// An array of JsonObject objects, which are serialzed GameObjects
/// </summary>
public JsonArray Objects { get; set; }
/// <summary>
/// The bounds are used to work out where to place the duplication, so it
/// doesn't clip through the floor.
/// </summary>
public BBox Bounds { get; set; }
/// <summary>
/// Describes where to draw a model for the preview
/// </summary>
public record struct PreviewModel( Model Model, Transform Transform, Transform[] Bones, BBox Bounds );
/// <summary>
/// A list of preview models to help visualze where the duplication will be placed
/// </summary>
public List<PreviewModel> PreviewModels { get; set; }
/// <summary>
/// Packages used in this
/// </summary>
public List<string> Packages { get; set; }
/// <summary>
/// Create DuplicationData from a bunch of objects.
/// center is the transform to use as the origin for the duplication.
/// The rotation of center should be the player's view yaw when they made the selection.
/// </summary>
public static DuplicationData CreateFromObjects( IEnumerable<GameObject> objects, Transform center )
{
var dupe = new DuplicationData();
dupe.Objects = new JsonArray();
dupe.Bounds = BBox.FromPositionAndSize( 0, 0.01f );
dupe.PreviewModels = new();
List<BBox> worldBounds = new List<BBox>();
foreach ( var obj in objects )
{
var entry = obj.Serialize();
worldBounds.Add( GetWorldBounds( obj ) );
var localized = center.ToLocal( obj.WorldTransform );
entry["Position"] = JsonValue.Create( localized.Position );
entry["Rotation"] = JsonValue.Create( localized.Rotation );
entry["Scale"] = JsonValue.Create( localized.Scale );
dupe.Objects.Add( entry );
foreach ( var renderer in obj.GetComponentsInChildren<ModelRenderer>() )
{
var model = renderer.Model ?? Model.Cube;
if ( model.IsError ) continue;
Transform[] bones = null;
if ( renderer is SkinnedModelRenderer skinned )
{
bones = skinned.GetBoneTransforms( false );
}
var modelTx = center.ToLocal( renderer.WorldTransform );
dupe.PreviewModels.Add( new DuplicationData.PreviewModel( model, modelTx, bones, model.Bounds ) );
}
}
if ( worldBounds.Count > 0 )
{
var txi = new Transform( -center.Position, center.Rotation.Inverse );
dupe.Bounds = BBox.FromBoxes( worldBounds.Select( x => x.Transform( txi ) ) );
}
var packages = Cloud.ResolvePrimaryAssetsFromJson( dupe.Objects );
dupe.Packages = packages.Select( x => x.FullIdent ).ToList();
return dupe;
}
public static BBox GetWorldBounds( GameObject go )
{
BBox box = BBox.FromPositionAndSize( 0, 0.01f );
var rb = go.GetComponentsInChildren<Collider>( false, true ).ToArray();
if ( rb.Length > 0 )
{
box = rb[0].GetWorldBounds();
foreach ( var b in rb )
{
box = box.AddBBox( b.GetWorldBounds() );
}
}
return box;
}
}