using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Unity.Pipeline.Commands;
using Unity.Pipeline.Editor.Authoring;
using Unity.Pipeline.Models;
using UnityEditor;
using UnityEngine;
namespace Unity.Pipeline.Editor.Commands.GameObjects
{
///
/// Component authoring commands (CLI-192): add/remove a component on a GameObject and get/set its
/// serialized properties.
///
/// WHY property get/set goes exclusively through /
/// : it is the only mutation path that dirties the object, records
/// prefab overrides, and registers a single Undo step (via +
/// ) the way the Inspector does. Direct
/// reflection on backing fields would silently desync the Editor. Component types are resolved by
/// name across all loaded assemblies () so agents can use short or
/// fully-qualified names.
///
public static class ComponentCommands
{
///
/// Add a component (resolved by type name) to a GameObject. Registered with
/// via the non-generic overload so the addition
/// is reversible. Returns the new component's identity so its properties can be set next.
///
[CliCommand("add_component", "Add a component (by type name) to a GameObject.")]
public static AuthoringResult AddComponent(
[CliArg("target", "Handle of the GameObject.", Required = true)] ObjectRef target,
[CliArg("type", "Component type name (e.g. 'Rigidbody' or 'UnityEngine.Camera').", Required = true)] string type)
{
var go = GameObjectCommands.ResolveGameObject(target, "target");
var componentType = TypeResolver.ResolveComponentType(type);
if (componentType == null)
throw new ArgumentException($"Could not resolve component type '{type}'.");
using (new AuthoringUndoScope("Add Component"))
{
var component = Undo.AddComponent(go, componentType);
if (component == null)
throw new InvalidOperationException(
$"Failed to add component '{componentType.Name}' to '{go.name}' (it may be disallowed on this GameObject).");
GameObjectCommands.MarkDirty(go);
return ObjectResolver.Describe(component);
}
}
///
/// Remove a component from a GameObject. The component is addressed directly by handle (an
/// resolving to a Component), or by GameObject handle plus a type name.
/// Uses so the removal is reversible.
///
[CliCommand("remove_component",
"Remove a component from a GameObject. Provide either a component handle (target) or a GameObject handle (target) plus a type name.")]
public static AuthoringResult RemoveComponent(
[CliArg("target", "Handle of the component to remove, OR of the GameObject when 'type' is given.", Required = true)] ObjectRef target,
[CliArg("type", "Component type name to remove from the target GameObject (omit when 'target' already points at a component).")] string type = null)
{
var component = ResolveComponent(target, type);
var go = component.gameObject;
var described = ObjectResolver.Describe(component);
using (new AuthoringUndoScope("Remove Component"))
{
Undo.DestroyObjectImmediate(component);
GameObjectCommands.MarkDirty(go);
}
return described;
}
///
/// Read the serialized properties of a component as a JSON map. Iterates the visible serialized
/// surface (skipping the script reference) and converts each property with
/// .
///
[CliCommand("get_component_properties",
"Get a component's serialized properties as a JSON map. Address the component by handle, or by GameObject handle + type.")]
public static ComponentPropertiesResult GetComponentProperties(
[CliArg("target", "Handle of the component, OR of the GameObject when 'type' is given.", Required = true)] ObjectRef target,
[CliArg("type", "Component type name on the target GameObject (omit when 'target' is a component handle).")] string type = null)
{
var component = ResolveComponent(target, type);
var so = new SerializedObject(component);
var properties = new Dictionary();
var iterator = so.GetIterator();
var enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false; // only iterate top-level visible properties
if (iterator.name == "m_Script")
continue;
try
{
properties[iterator.name] = SerializedPropertyConverter.Read(iterator.Copy());
}
catch (Exception ex)
{
properties[iterator.name] = new JValue($"");
}
}
return new ComponentPropertiesResult
{
Component = ObjectResolver.Describe(component),
Properties = properties
};
}
///
/// Set one or more serialized properties on a component. Each entry in
/// maps a property path (e.g. "m_Mass" or "myField") to a JSON value. The whole batch is one
/// Undo step: snapshots the component, every property is written
/// through , and
/// commits + dirties as one operation.
/// An unknown property name fails the whole batch with a clear error (no partial apply).
///
[CliCommand("set_component_properties",
"Set serialized properties on a component (one Undo step). 'properties' maps property name -> value; object references accept an ObjectRef handle.")]
public static ComponentPropertiesResult SetComponentProperties(
[CliArg("target", "Handle of the component, OR of the GameObject when 'type' is given.", Required = true)] ObjectRef target,
[CliArg("properties", "Map of serialized property name to value. Vectors/colors are arrays; object refs are handle objects.", Required = true)] JObject properties,
[CliArg("type", "Component type name on the target GameObject (omit when 'target' is a component handle).")] string type = null)
{
if (properties == null || properties.Count == 0)
throw new ArgumentException("'properties' must contain at least one property to set.");
var component = ResolveComponent(target, type);
using (new AuthoringUndoScope("Set Component Properties"))
{
Undo.RecordObject(component, "Set Component Properties");
var so = new SerializedObject(component);
foreach (var pair in properties)
{
var property = so.FindProperty(pair.Key);
if (property == null)
throw new ArgumentException(
$"Component '{component.GetType().Name}' has no serialized property '{pair.Key}'.");
SerializedPropertyConverter.Write(property, pair.Value);
}
// Applies the changes, registers Undo, and dirties the object/scene.
so.ApplyModifiedProperties();
GameObjectCommands.MarkDirty(component.gameObject);
}
// Re-read so the caller sees the committed values.
return GetComponentProperties(target, type);
}
#region Helpers
///
/// Resolve a component either directly (the handle points at a Component) or by GameObject
/// handle + type name. Throws a descriptive error when neither resolves.
///
private static Component ResolveComponent(ObjectRef handle, string type)
{
if (!ObjectResolver.TryResolve(handle, out var obj, out var error))
throw new ArgumentException($"Could not resolve 'target': {error}");
// The handle already points at a specific Component: honour it directly so we never
// mutate/remove the wrong instance when the GameObject has several components of the same
// type. A 'type' here is only a constraint to validate against, NOT a reason to re-fetch
// GetComponent(type) (which returns the first match and would ignore the handle).
if (obj is Component directComponent)
{
if (!string.IsNullOrEmpty(type))
{
var requestedType = TypeResolver.ResolveComponentType(type);
if (requestedType == null)
throw new ArgumentException($"Could not resolve component type '{type}'.");
if (!requestedType.IsInstanceOfType(directComponent))
throw new ArgumentException(
$"'target' resolves to a {directComponent.GetType().Name}, which is not a '{requestedType.Name}'.");
}
return directComponent;
}
var go = obj as GameObject;
if (go == null)
throw new ArgumentException($"'target' did not resolve to a GameObject or Component (got {obj.GetType().Name}).");
if (string.IsNullOrEmpty(type))
throw new ArgumentException("'type' is required when 'target' is a GameObject.");
var componentType = TypeResolver.ResolveComponentType(type);
if (componentType == null)
throw new ArgumentException($"Could not resolve component type '{type}'.");
var component = go.GetComponent(componentType);
if (component == null)
throw new ArgumentException($"GameObject '{go.name}' has no component of type '{componentType.Name}'.");
return component;
}
#endregion
}
}