Add Unity Pipeline package support
This commit is contained in:
+211
@@ -0,0 +1,211 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="SerializedObject"/> /
|
||||
/// <see cref="SerializedProperty"/>: it is the only mutation path that dirties the object, records
|
||||
/// prefab overrides, and registers a single Undo step (via <see cref="Undo.RecordObject"/> +
|
||||
/// <see cref="SerializedObject.ApplyModifiedProperties"/>) 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 (<see cref="TypeResolver"/>) so agents can use short or
|
||||
/// fully-qualified names.
|
||||
/// </summary>
|
||||
public static class ComponentCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a component (resolved by type name) to a GameObject. Registered with
|
||||
/// <see cref="Undo.AddComponent{T}(GameObject)"/> via the non-generic overload so the addition
|
||||
/// is reversible. Returns the new component's identity so its properties can be set next.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a component from a GameObject. The component is addressed directly by handle (an
|
||||
/// <see cref="ObjectRef"/> resolving to a Component), or by GameObject handle plus a type name.
|
||||
/// Uses <see cref="Undo.DestroyObjectImmediate"/> so the removal is reversible.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="SerializedPropertyConverter.Read"/>.
|
||||
/// </summary>
|
||||
[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<string, JToken>();
|
||||
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($"<error:{ex.Message}>");
|
||||
}
|
||||
}
|
||||
|
||||
return new ComponentPropertiesResult
|
||||
{
|
||||
Component = ObjectResolver.Describe(component),
|
||||
Properties = properties
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set one or more serialized properties on a component. Each entry in <paramref name="properties"/>
|
||||
/// maps a property path (e.g. "m_Mass" or "myField") to a JSON value. The whole batch is one
|
||||
/// Undo step: <see cref="Undo.RecordObject"/> snapshots the component, every property is written
|
||||
/// through <see cref="SerializedPropertyConverter.Write"/>, and
|
||||
/// <see cref="SerializedObject.ApplyModifiedProperties"/> commits + dirties as one operation.
|
||||
/// An unknown property name fails the whole batch with a clear error (no partial apply).
|
||||
/// </summary>
|
||||
[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
|
||||
|
||||
/// <summary>
|
||||
/// Resolve a component either directly (the handle points at a Component) or by GameObject
|
||||
/// handle + type name. Throws a descriptive error when neither resolves.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a7470ef26f8497595bcbdb84ceb96dc
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Unity.Pipeline.Models;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Structured result for <c>get_component_properties</c> / <c>set_component_properties</c>.
|
||||
/// WHY it pairs the component identity with the property map: the caller gets back both the
|
||||
/// canonical <see cref="AuthoringResult"/> handle (to reference the component again) and the
|
||||
/// current serialized values in one round trip, which also makes set return the committed state
|
||||
/// for a read-after-write check.
|
||||
/// </summary>
|
||||
public sealed class ComponentPropertiesResult
|
||||
{
|
||||
/// <summary>Canonical identity of the component the properties belong to.</summary>
|
||||
[JsonProperty("component")]
|
||||
public AuthoringResult Component { get; set; }
|
||||
|
||||
/// <summary>Map of serialized property name to its JSON value.</summary>
|
||||
[JsonProperty("properties")]
|
||||
public Dictionary<string, JToken> Properties { get; set; } = new Dictionary<string, JToken>();
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e87b3f2873ff4bac9229a28242917ac0
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Unity.Pipeline.Models;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Structured result for the batch <c>create_gameobjects</c> command (CLI-223). WHY a dedicated
|
||||
/// envelope rather than a bare array: it carries the created <see cref="Count"/> alongside the
|
||||
/// list so an agent can confirm the whole batch landed in one round-trip without inspecting array
|
||||
/// length, and mirrors <see cref="FindGameObjectsResult"/> so the GameObject command surface stays
|
||||
/// consistent. Each entry is the same canonical <see cref="AuthoringResult"/> identity returned by
|
||||
/// the single-object <c>create_gameobject</c>, so any created object can be fed straight back as an
|
||||
/// <see cref="ObjectRef"/> in a follow-up call.
|
||||
/// </summary>
|
||||
public sealed class CreateGameObjectsResult
|
||||
{
|
||||
/// <summary>Number of GameObjects created in the batch.</summary>
|
||||
[JsonProperty("count")]
|
||||
public int Count { get; set; }
|
||||
|
||||
/// <summary>Canonical identities of the created GameObjects, in creation order.</summary>
|
||||
[JsonProperty("gameObjects")]
|
||||
public List<AuthoringResult> GameObjects { get; set; } = new List<AuthoringResult>();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 671f5065c22d6474b80053b6da5a84ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Unity.Pipeline.Models;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Structured result for <c>find_gameobjects</c>. WHY a dedicated envelope rather than a bare
|
||||
/// array: it carries the match <see cref="Count"/> alongside the list so an agent can branch on
|
||||
/// "found / not found / ambiguous" without inspecting array length, and leaves room to add query
|
||||
/// metadata later without breaking the shape. Each entry is the same canonical
|
||||
/// <see cref="AuthoringResult"/> identity returned by the single-object commands, so a result can
|
||||
/// be fed straight back as an <see cref="ObjectRef"/> in a follow-up call.
|
||||
/// </summary>
|
||||
public sealed class FindGameObjectsResult
|
||||
{
|
||||
/// <summary>Number of GameObjects that matched the query.</summary>
|
||||
[JsonProperty("count")]
|
||||
public int Count { get; set; }
|
||||
|
||||
/// <summary>Canonical identities of the matching GameObjects.</summary>
|
||||
[JsonProperty("gameObjects")]
|
||||
public List<AuthoringResult> GameObjects { get; set; } = new List<AuthoringResult>();
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d12bf4a53c14a839c26a91e8ec68292
|
||||
+533
@@ -0,0 +1,533 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Unity.Pipeline.Commands;
|
||||
using Unity.Pipeline.Editor.Authoring;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// GameObject authoring commands (CLI-192): creating GameObjects, querying the scene hierarchy,
|
||||
/// and mutating the core GameObject/Transform surface (parenting, transform, active state, tag,
|
||||
/// layer, name, deletion).
|
||||
///
|
||||
/// WHY these go through the same conventions as the rest of the authoring foundation:
|
||||
/// every mutation is wrapped in an <see cref="AuthoringUndoScope"/> and registered with Unity's
|
||||
/// Undo system so an agent's multi-step action reverts as a single collapsible step. Each
|
||||
/// mutated object is marked dirty (and its scene flagged dirty) so the change survives a Save and
|
||||
/// is visible to the rest of the Editor. Results come back as the canonical
|
||||
/// <see cref="AuthoringResult"/> envelope (via <see cref="ObjectResolver.Describe"/>) so the
|
||||
/// agent can address the same object in a follow-up call.
|
||||
/// </summary>
|
||||
public static class GameObjectCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Create an empty GameObject or a built-in primitive in the active scene.
|
||||
///
|
||||
/// Creation goes through <see cref="GameObject.CreatePrimitive"/> for primitives (so the
|
||||
/// matching mesh + collider are attached exactly as the Editor would) and a plain
|
||||
/// <c>new GameObject</c> for the empty case. The object is registered with
|
||||
/// <see cref="Undo.RegisterCreatedObjectUndo"/> so it can be reverted, and an optional parent
|
||||
/// is resolved by handle so a whole hierarchy can be built in one sequence of calls.
|
||||
/// </summary>
|
||||
[CliCommand("create_gameobject",
|
||||
"Create an empty GameObject or a built-in primitive (cube/sphere/capsule/cylinder/plane/quad) in the active scene.")]
|
||||
public static AuthoringResult CreateGameObject(
|
||||
[CliArg("name", "Name for the new GameObject. Defaults to 'GameObject' (or the primitive name).")] string name = null,
|
||||
[CliArg("primitive", "Optional primitive type: cube, sphere, capsule, cylinder, plane, quad. Omit for an empty GameObject.")] string primitive = null,
|
||||
[CliArg("parent", "Optional parent handle (globalId/path/guid/instanceId/hierarchyPath). The new object becomes a child of it.")] ObjectRef parent = null)
|
||||
{
|
||||
// Resolve the parent once, outside the scope, so a bad handle fails before any creation.
|
||||
var parentTransform = ResolveParentTransform(parent);
|
||||
|
||||
using (new AuthoringUndoScope("Create GameObject"))
|
||||
{
|
||||
var go = CreateOne(primitive, name, parentTransform);
|
||||
MarkDirty(go);
|
||||
return ObjectResolver.Describe(go);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create N GameObjects (empty or a primitive) in one server round-trip (CLI-223).
|
||||
///
|
||||
/// WHY a separate plural command rather than overloading <see cref="CreateGameObject"/>: the
|
||||
/// single command's contract — one call returns one <see cref="AuthoringResult"/> — is relied
|
||||
/// on by existing callers and tests, so it is left untouched. This plural command is the batch
|
||||
/// surface and returns a <see cref="CreateGameObjectsResult"/> envelope (count + per-item
|
||||
/// identities). When <paramref name="count"/> > 1 and no explicit name array is supplied,
|
||||
/// each object is suffixed Name1..NameN so they remain individually addressable.
|
||||
///
|
||||
/// Per-index <paramref name="positions"/>/<paramref name="rotations"/>/<paramref name="scales"/>
|
||||
/// are arrays of [x,y,z] vectors. When a vectors array is supplied its length MUST equal
|
||||
/// <paramref name="count"/> (a mismatch is a validation error). Omitted channels leave the
|
||||
/// object at its creation default (origin / identity / unit scale). The whole batch is wrapped
|
||||
/// in ONE <see cref="AuthoringUndoScope"/> so it reverts as a single Undo step.
|
||||
/// </summary>
|
||||
[CliCommand("create_gameobjects",
|
||||
"Batch-create N empty GameObjects or primitives in one call. Optional positions/rotations/scales are arrays of [x,y,z] (length must equal count). Returns the created identities.")]
|
||||
public static CreateGameObjectsResult CreateGameObjects(
|
||||
[CliArg("name", "Base name. With count>1 and no explicit names, objects are suffixed Name1..NameN.")] string name = null,
|
||||
[CliArg("primitive", "Optional primitive type: cube, sphere, capsule, cylinder, plane, quad. Omit for empty GameObjects.")] string primitive = null,
|
||||
[CliArg("parent", "Optional parent handle. Every created object becomes a child of it.")] ObjectRef parent = null,
|
||||
[CliArg("count", "How many GameObjects to create. Default 1.")] int count = 1,
|
||||
[CliArg("positions", "Local positions, one [x,y,z] per object. Length must equal count when supplied.")] float[][] positions = null,
|
||||
[CliArg("rotations", "Local Euler rotations (degrees), one [x,y,z] per object. Length must equal count when supplied.")] float[][] rotations = null,
|
||||
[CliArg("scales", "Local scales, one [x,y,z] per object. Length must equal count when supplied.")] float[][] scales = null)
|
||||
{
|
||||
if (count < 1)
|
||||
throw new ArgumentException($"'count' must be at least 1, got {count}.");
|
||||
|
||||
ValidateVectorArray(positions, count, "positions");
|
||||
ValidateVectorArray(rotations, count, "rotations");
|
||||
ValidateVectorArray(scales, count, "scales");
|
||||
|
||||
// Resolve the parent once, outside the scope, so a bad handle fails before any creation.
|
||||
var parentTransform = ResolveParentTransform(parent);
|
||||
|
||||
var result = new CreateGameObjectsResult();
|
||||
using (new AuthoringUndoScope("Create GameObjects"))
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
// count==1 keeps the bare name (no "1" suffix) so a single-item batch reads
|
||||
// naturally; count>1 suffixes 1..N for individual addressability.
|
||||
var itemName = count > 1 && !string.IsNullOrEmpty(name) ? name + (i + 1) : name;
|
||||
var go = CreateOne(primitive, itemName, parentTransform);
|
||||
|
||||
// Index the arg name so a null/mis-shaped entry names the offending element
|
||||
// (a JSON array may contain a null entry even when its length matches count).
|
||||
if (positions != null)
|
||||
go.transform.localPosition = ToVector3(positions[i], $"positions[{i}]");
|
||||
if (rotations != null)
|
||||
go.transform.localEulerAngles = ToVector3(rotations[i], $"rotations[{i}]");
|
||||
if (scales != null)
|
||||
go.transform.localScale = ToVector3(scales[i], $"scales[{i}]");
|
||||
|
||||
MarkDirty(go);
|
||||
result.GameObjects.Add(ObjectResolver.Describe(go));
|
||||
}
|
||||
}
|
||||
|
||||
result.Count = result.GameObjects.Count;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find GameObjects in the loaded scenes by name, tag, component type, and/or hierarchy path.
|
||||
/// All supplied filters are combined (AND). Returns the canonical identity of each match so an
|
||||
/// agent can act on the results without a second lookup.
|
||||
///
|
||||
/// This is intentionally a structured query rather than a single-object resolve: agents need
|
||||
/// to discover what exists before they can reference it, and the result set carries the same
|
||||
/// <see cref="AuthoringResult"/> identities used everywhere else.
|
||||
/// </summary>
|
||||
[CliCommand("find_gameobjects",
|
||||
"Find GameObjects in loaded scenes by name, tag, component type, and/or hierarchy path (filters are combined). Returns structured identities.")]
|
||||
public static FindGameObjectsResult FindGameObjects(
|
||||
[CliArg("name", "Exact name to match.")] string name = null,
|
||||
[CliArg("tag", "Tag to match (e.g. 'Player').")] string tag = null,
|
||||
[CliArg("type", "Component type name to match (e.g. 'Rigidbody', 'UnityEngine.Camera').")] string type = null,
|
||||
[CliArg("hierarchy_path", "Exact hierarchy path to match (e.g. '/Root/Child').")] string hierarchyPath = null,
|
||||
[CliArg("include_inactive", "Include inactive GameObjects. Default true.")] bool includeInactive = true)
|
||||
{
|
||||
Type componentType = null;
|
||||
if (!string.IsNullOrEmpty(type))
|
||||
{
|
||||
componentType = TypeResolver.ResolveComponentType(type);
|
||||
if (componentType == null)
|
||||
throw new ArgumentException($"Could not resolve component type '{type}'.");
|
||||
}
|
||||
|
||||
// Resolve the hierarchy filter once so it is comparable to each object's computed path.
|
||||
var hierarchyFilter = string.IsNullOrEmpty(hierarchyPath) ? null : NormalizeHierarchyPath(hierarchyPath);
|
||||
|
||||
// Validate the tag up front: comparing against an undefined tag (CompareTag / .tag) would
|
||||
// log a Unity error per object. An unknown tag simply yields no matches.
|
||||
if (!string.IsNullOrEmpty(tag) && !InternalEditorTagExists(tag))
|
||||
return new FindGameObjectsResult { Count = 0, GameObjects = new List<AuthoringResult>() };
|
||||
|
||||
var matches = new List<AuthoringResult>();
|
||||
foreach (var go in EnumerateSceneGameObjects(includeInactive))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(name) && go.name != name)
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrEmpty(tag) && go.tag != tag)
|
||||
continue;
|
||||
|
||||
if (componentType != null && go.GetComponent(componentType) == null)
|
||||
continue;
|
||||
|
||||
if (hierarchyFilter != null && GetHierarchyPath(go) != hierarchyFilter)
|
||||
continue;
|
||||
|
||||
var described = ObjectResolver.Describe(go);
|
||||
if (described != null)
|
||||
matches.Add(described);
|
||||
}
|
||||
|
||||
return new FindGameObjectsResult { Count = matches.Count, GameObjects = matches };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the local transform (position, rotation in Euler degrees, scale) of a GameObject.
|
||||
/// Any omitted channel is left unchanged. Goes through <see cref="Undo.RegisterCompleteObjectUndo"/>
|
||||
/// on the Transform so the change is reversible and recorded as a prefab override where relevant.
|
||||
/// </summary>
|
||||
[CliCommand("set_transform",
|
||||
"Set a GameObject's local position/rotation(euler)/scale. Omitted channels are left unchanged.")]
|
||||
public static AuthoringResult SetTransform(
|
||||
[CliArg("target", "Handle of the GameObject to modify.", Required = true)] ObjectRef target,
|
||||
[CliArg("position", "Local position as [x,y,z].")] float[] position = null,
|
||||
[CliArg("rotation", "Local rotation as Euler angles [x,y,z] in degrees.")] float[] rotation = null,
|
||||
[CliArg("scale", "Local scale as [x,y,z].")] float[] scale = null)
|
||||
{
|
||||
var go = ResolveGameObject(target, "target");
|
||||
using (new AuthoringUndoScope("Set Transform"))
|
||||
{
|
||||
var transform = go.transform;
|
||||
Undo.RegisterCompleteObjectUndo(transform, "Set Transform");
|
||||
|
||||
if (position != null)
|
||||
transform.localPosition = ToVector3(position, "position");
|
||||
if (rotation != null)
|
||||
transform.localEulerAngles = ToVector3(rotation, "rotation");
|
||||
if (scale != null)
|
||||
transform.localScale = ToVector3(scale, "scale");
|
||||
|
||||
MarkDirty(go);
|
||||
return ObjectResolver.Describe(go);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reparent a GameObject (or clear its parent so it becomes a scene root). Uses
|
||||
/// <see cref="Undo.SetTransformParent"/> so the reparent — including the sibling-index change —
|
||||
/// is a single undoable operation. World position is preserved by default to match the
|
||||
/// Editor's drag-to-reparent behavior.
|
||||
/// </summary>
|
||||
[CliCommand("set_parent",
|
||||
"Reparent a GameObject under a new parent, or detach it to scene root when no parent is given.")]
|
||||
public static AuthoringResult SetParent(
|
||||
[CliArg("target", "Handle of the GameObject to reparent.", Required = true)] ObjectRef target,
|
||||
[CliArg("parent", "Handle of the new parent. Omit (or empty) to move the object to the scene root.")] ObjectRef parent = null,
|
||||
[CliArg("world_position_stays", "Keep the object's world position when reparenting. Default true.")] bool worldPositionStays = true)
|
||||
{
|
||||
var go = ResolveGameObject(target, "target");
|
||||
Transform parentTransform = null;
|
||||
if (parent != null && !parent.IsEmpty)
|
||||
parentTransform = ResolveGameObject(parent, "parent").transform;
|
||||
|
||||
using (new AuthoringUndoScope("Set Parent"))
|
||||
{
|
||||
Undo.SetTransformParent(go.transform, parentTransform, worldPositionStays, "Set Parent");
|
||||
MarkDirty(go);
|
||||
return ObjectResolver.Describe(go);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Set a GameObject's active self-state.</summary>
|
||||
[CliCommand("set_active", "Set a GameObject's active self-state (activeSelf).")]
|
||||
public static AuthoringResult SetActive(
|
||||
[CliArg("target", "Handle of the GameObject.", Required = true)] ObjectRef target,
|
||||
[CliArg("active", "Desired active state.", Required = true)] bool active)
|
||||
{
|
||||
var go = ResolveGameObject(target, "target");
|
||||
using (new AuthoringUndoScope("Set Active"))
|
||||
{
|
||||
Undo.RegisterCompleteObjectUndo(go, "Set Active");
|
||||
go.SetActive(active);
|
||||
MarkDirty(go);
|
||||
return ObjectResolver.Describe(go);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a GameObject's tag. The tag must already exist in the project's Tag Manager; an unknown
|
||||
/// tag surfaces a clear error rather than silently creating one (tag creation is a project
|
||||
/// settings mutation outside this command's scope).
|
||||
/// </summary>
|
||||
[CliCommand("set_tag", "Set a GameObject's tag (the tag must already exist in the project).")]
|
||||
public static AuthoringResult SetTag(
|
||||
[CliArg("target", "Handle of the GameObject.", Required = true)] ObjectRef target,
|
||||
[CliArg("tag", "Tag to assign (must exist in the Tag Manager).", Required = true)] string tag)
|
||||
{
|
||||
var go = ResolveGameObject(target, "target");
|
||||
if (!InternalEditorTagExists(tag))
|
||||
throw new ArgumentException($"Tag '{tag}' does not exist in the project. Add it via the Tag Manager first.");
|
||||
|
||||
using (new AuthoringUndoScope("Set Tag"))
|
||||
{
|
||||
Undo.RegisterCompleteObjectUndo(go, "Set Tag");
|
||||
go.tag = tag;
|
||||
MarkDirty(go);
|
||||
return ObjectResolver.Describe(go);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a GameObject's layer by name or numeric index. A name is resolved via
|
||||
/// <see cref="LayerMask.NameToLayer"/>; an out-of-range index or unknown name is rejected.
|
||||
/// </summary>
|
||||
[CliCommand("set_layer", "Set a GameObject's layer by name or numeric index (0-31).")]
|
||||
public static AuthoringResult SetLayer(
|
||||
[CliArg("target", "Handle of the GameObject.", Required = true)] ObjectRef target,
|
||||
[CliArg("layer", "Layer name (e.g. 'UI') or numeric index 0-31.", Required = true)] string layer)
|
||||
{
|
||||
var go = ResolveGameObject(target, "target");
|
||||
var layerIndex = ResolveLayer(layer);
|
||||
|
||||
using (new AuthoringUndoScope("Set Layer"))
|
||||
{
|
||||
Undo.RegisterCompleteObjectUndo(go, "Set Layer");
|
||||
go.layer = layerIndex;
|
||||
MarkDirty(go);
|
||||
return ObjectResolver.Describe(go);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Rename a GameObject.</summary>
|
||||
[CliCommand("rename_gameobject", "Rename a GameObject.")]
|
||||
public static AuthoringResult RenameGameObject(
|
||||
[CliArg("target", "Handle of the GameObject.", Required = true)] ObjectRef target,
|
||||
[CliArg("name", "New name.", Required = true)] string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("New name must not be empty.");
|
||||
|
||||
var go = ResolveGameObject(target, "target");
|
||||
using (new AuthoringUndoScope("Rename GameObject"))
|
||||
{
|
||||
Undo.RegisterCompleteObjectUndo(go, "Rename GameObject");
|
||||
go.name = name;
|
||||
MarkDirty(go);
|
||||
return ObjectResolver.Describe(go);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a GameObject from the scene. Uses <see cref="Undo.DestroyObjectImmediate"/> so the
|
||||
/// deletion is reversible. The result describes the object's identity captured *before*
|
||||
/// destruction (the live object is gone afterwards).
|
||||
/// </summary>
|
||||
[CliCommand("delete_gameobject", "Delete a GameObject from the scene (reversible via Undo).")]
|
||||
public static AuthoringResult DeleteGameObject(
|
||||
[CliArg("target", "Handle of the GameObject to delete.", Required = true)] ObjectRef target)
|
||||
{
|
||||
var go = ResolveGameObject(target, "target");
|
||||
// Capture identity before destruction; the live object is invalid afterwards.
|
||||
var described = ObjectResolver.Describe(go);
|
||||
var scene = go.scene;
|
||||
|
||||
using (new AuthoringUndoScope("Delete GameObject"))
|
||||
{
|
||||
Undo.DestroyObjectImmediate(go);
|
||||
if (scene.IsValid())
|
||||
EditorSceneManager.MarkSceneDirty(scene);
|
||||
}
|
||||
|
||||
return described;
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Resolve an <see cref="ObjectRef"/> to a GameObject. Accepts either a GameObject handle or a
|
||||
/// Component handle (in which case its owning GameObject is used), matching how agents tend to
|
||||
/// hold references. Throws a descriptive error rather than returning null so the command
|
||||
/// surfaces a clear message to the caller.
|
||||
/// </summary>
|
||||
internal static GameObject ResolveGameObject(ObjectRef handle, string argName)
|
||||
{
|
||||
if (!ObjectResolver.TryResolve(handle, out var obj, out var error))
|
||||
throw new ArgumentException($"Could not resolve '{argName}': {error}");
|
||||
|
||||
var go = obj as GameObject ?? (obj as Component)?.gameObject;
|
||||
if (go == null)
|
||||
throw new ArgumentException($"'{argName}' did not resolve to a GameObject (got {obj.GetType().Name}).");
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
/// <summary>Mark a GameObject and its scene dirty so changes persist and the Editor refreshes.</summary>
|
||||
internal static void MarkDirty(GameObject go)
|
||||
{
|
||||
EditorUtility.SetDirty(go);
|
||||
if (go.scene.IsValid())
|
||||
EditorSceneManager.MarkSceneDirty(go.scene);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a single GameObject (empty or a primitive), apply an optional name, register it for
|
||||
/// Undo, and parent it under <paramref name="parentTransform"/> when given. Shared by the
|
||||
/// single <see cref="CreateGameObject"/> and the batch <see cref="CreateGameObjects"/> so both
|
||||
/// build objects identically. MUST be called inside an <see cref="AuthoringUndoScope"/>.
|
||||
/// </summary>
|
||||
private static GameObject CreateOne(string primitive, string name, Transform parentTransform)
|
||||
{
|
||||
GameObject go;
|
||||
if (!string.IsNullOrEmpty(primitive))
|
||||
{
|
||||
if (!TryParsePrimitive(primitive, out var primitiveType))
|
||||
throw new ArgumentException(
|
||||
$"Unknown primitive '{primitive}'. Expected one of: cube, sphere, capsule, cylinder, plane, quad.");
|
||||
|
||||
go = GameObject.CreatePrimitive(primitiveType);
|
||||
}
|
||||
else
|
||||
{
|
||||
go = new GameObject();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
go.name = name;
|
||||
|
||||
Undo.RegisterCreatedObjectUndo(go, "Create GameObject");
|
||||
|
||||
if (parentTransform != null)
|
||||
Undo.SetTransformParent(go.transform, parentTransform, "Create GameObject");
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve an optional parent handle to its Transform, or null when no parent is supplied.
|
||||
/// Resolved before any creation so an invalid handle fails fast (and a batch creates nothing).
|
||||
/// </summary>
|
||||
private static Transform ResolveParentTransform(ObjectRef parent)
|
||||
{
|
||||
if (parent == null || parent.IsEmpty)
|
||||
return null;
|
||||
return ResolveGameObject(parent, "parent").transform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a per-item vectors array (positions/rotations/scales) up front, before any object is
|
||||
/// created. When supplied it must have exactly one entry per object, and every entry must be a
|
||||
/// non-null [x,y,z] triple (a JSON array can carry a null element even when its length matches
|
||||
/// count). A bad array is an agent error surfaced as an <see cref="ArgumentException"/> naming
|
||||
/// the offending index, so a batch applies fully or creates nothing rather than throwing an
|
||||
/// NRE part-way through.
|
||||
/// </summary>
|
||||
private static void ValidateVectorArray(float[][] vectors, int count, string argName)
|
||||
{
|
||||
if (vectors == null)
|
||||
return;
|
||||
|
||||
if (vectors.Length != count)
|
||||
throw new ArgumentException(
|
||||
$"'{argName}' has {vectors.Length} entries but count is {count}; they must match.");
|
||||
|
||||
for (int i = 0; i < vectors.Length; i++)
|
||||
{
|
||||
if (vectors[i] == null)
|
||||
throw new ArgumentException($"'{argName}[{i}]' must be an [x,y,z] array but was null.");
|
||||
if (vectors[i].Length != 3)
|
||||
throw new ArgumentException(
|
||||
$"'{argName}[{i}]' must have exactly 3 components [x,y,z], got {vectors[i].Length}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParsePrimitive(string value, out PrimitiveType primitiveType)
|
||||
{
|
||||
switch (value.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "cube": primitiveType = PrimitiveType.Cube; return true;
|
||||
case "sphere": primitiveType = PrimitiveType.Sphere; return true;
|
||||
case "capsule": primitiveType = PrimitiveType.Capsule; return true;
|
||||
case "cylinder": primitiveType = PrimitiveType.Cylinder; return true;
|
||||
case "plane": primitiveType = PrimitiveType.Plane; return true;
|
||||
case "quad": primitiveType = PrimitiveType.Quad; return true;
|
||||
default: primitiveType = default; return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 ToVector3(float[] values, string argName)
|
||||
{
|
||||
if (values == null)
|
||||
throw new ArgumentException($"'{argName}' must be an [x,y,z] array but was null.");
|
||||
if (values.Length != 3)
|
||||
throw new ArgumentException($"'{argName}' must have exactly 3 components [x,y,z], got {values.Length}.");
|
||||
return new Vector3(values[0], values[1], values[2]);
|
||||
}
|
||||
|
||||
private static int ResolveLayer(string layer)
|
||||
{
|
||||
if (int.TryParse(layer, out var index))
|
||||
{
|
||||
if (index < 0 || index > 31)
|
||||
throw new ArgumentException($"Layer index {index} is out of range (0-31).");
|
||||
return index;
|
||||
}
|
||||
|
||||
var named = LayerMask.NameToLayer(layer);
|
||||
if (named < 0)
|
||||
throw new ArgumentException($"Layer '{layer}' does not exist in the project.");
|
||||
return named;
|
||||
}
|
||||
|
||||
private static bool InternalEditorTagExists(string tag)
|
||||
{
|
||||
return UnityEditorInternal.InternalEditorUtility.tags.Contains(tag);
|
||||
}
|
||||
|
||||
private static IEnumerable<GameObject> EnumerateSceneGameObjects(bool includeInactive)
|
||||
{
|
||||
for (int s = 0; s < SceneManager.sceneCount; s++)
|
||||
{
|
||||
var scene = SceneManager.GetSceneAt(s);
|
||||
if (!scene.isLoaded)
|
||||
continue;
|
||||
|
||||
foreach (var root in scene.GetRootGameObjects())
|
||||
{
|
||||
foreach (var go in EnumerateRecursive(root, includeInactive))
|
||||
yield return go;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<GameObject> EnumerateRecursive(GameObject go, bool includeInactive)
|
||||
{
|
||||
if (!includeInactive && !go.activeInHierarchy)
|
||||
yield break;
|
||||
|
||||
yield return go;
|
||||
|
||||
var transform = go.transform;
|
||||
for (int i = 0; i < transform.childCount; i++)
|
||||
{
|
||||
foreach (var child in EnumerateRecursive(transform.GetChild(i).gameObject, includeInactive))
|
||||
yield return child;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetHierarchyPath(GameObject go)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(go.name);
|
||||
var parent = go.transform.parent;
|
||||
while (parent != null)
|
||||
{
|
||||
sb.Insert(0, parent.name + "/");
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return "/" + sb;
|
||||
}
|
||||
|
||||
private static string NormalizeHierarchyPath(string path)
|
||||
{
|
||||
var trimmed = "/" + path.Trim().Trim('/');
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1aac051d9234c78b4918a8dd366514a
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Unity.Pipeline.Editor.Authoring;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads and writes a single <see cref="SerializedProperty"/> from/to a JSON value.
|
||||
///
|
||||
/// WHY everything goes through SerializedProperty rather than reflection on the C# field: it is
|
||||
/// the only path that correctly participates in dirtying, prefab override tracking, and Undo
|
||||
/// (when the change is applied via <see cref="SerializedObject.ApplyModifiedProperties"/>).
|
||||
/// Reflection would bypass all three and leave the Editor's view stale.
|
||||
///
|
||||
/// Supported surface: primitives (bool/int/long/float/double/string), enums (by name or index),
|
||||
/// <see cref="Vector2"/>/<see cref="Vector3"/>/<see cref="Vector4"/>, <see cref="Color"/>, and
|
||||
/// object references (assigned from an <see cref="ObjectRef"/> handle resolved by
|
||||
/// <see cref="ObjectResolver"/>).
|
||||
/// </summary>
|
||||
internal static class SerializedPropertyConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Read a property into a JSON-friendly value (primitive, string, or small array/object for
|
||||
/// vector/color types). Unsupported property kinds return a short type descriptor string so a
|
||||
/// get_component_properties dump never throws on an exotic field.
|
||||
/// </summary>
|
||||
public static JToken Read(SerializedProperty property)
|
||||
{
|
||||
// Array/list fields surface as Generic with isArray=true; return their elements as a JSON
|
||||
// array so a get/set round-trips. (String also reports isArray, hence the exclusion.)
|
||||
if (property.isArray && property.propertyType != SerializedPropertyType.String)
|
||||
{
|
||||
var elements = new JArray();
|
||||
for (int i = 0; i < property.arraySize; i++)
|
||||
elements.Add(Read(property.GetArrayElementAtIndex(i)));
|
||||
return elements;
|
||||
}
|
||||
|
||||
switch (property.propertyType)
|
||||
{
|
||||
case SerializedPropertyType.Boolean:
|
||||
return new JValue(property.boolValue);
|
||||
case SerializedPropertyType.Integer:
|
||||
return new JValue(property.longValue);
|
||||
case SerializedPropertyType.Float:
|
||||
return new JValue(property.doubleValue);
|
||||
case SerializedPropertyType.String:
|
||||
return new JValue(property.stringValue);
|
||||
case SerializedPropertyType.Enum:
|
||||
// enumValueIndex indexes enumNames; guard against -1 (mixed/unknown).
|
||||
return property.enumValueIndex >= 0 && property.enumValueIndex < property.enumNames.Length
|
||||
? new JValue(property.enumNames[property.enumValueIndex])
|
||||
: new JValue(property.intValue);
|
||||
case SerializedPropertyType.Vector2:
|
||||
return ToArray(property.vector2Value.x, property.vector2Value.y);
|
||||
case SerializedPropertyType.Vector3:
|
||||
return ToArray(property.vector3Value.x, property.vector3Value.y, property.vector3Value.z);
|
||||
case SerializedPropertyType.Vector4:
|
||||
return ToArray(property.vector4Value.x, property.vector4Value.y, property.vector4Value.z, property.vector4Value.w);
|
||||
case SerializedPropertyType.Color:
|
||||
var c = property.colorValue;
|
||||
return ToArray(c.r, c.g, c.b, c.a);
|
||||
case SerializedPropertyType.ObjectReference:
|
||||
return property.objectReferenceValue != null
|
||||
? JToken.FromObject(ObjectResolver.Describe(property.objectReferenceValue))
|
||||
: JValue.CreateNull();
|
||||
default:
|
||||
// Bounds, Quaternion, AnimationCurve, etc. are out of MVP scope: surface a hint.
|
||||
return new JValue($"<unsupported:{property.propertyType}>");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a JSON value into a property. Throws <see cref="ArgumentException"/> with a clear
|
||||
/// message on a type mismatch so set_component_properties reports exactly which field/value
|
||||
/// failed. Object references are resolved from an <see cref="ObjectRef"/>-shaped JSON object
|
||||
/// (or a bare string treated as an asset path / globalId).
|
||||
/// </summary>
|
||||
public static void Write(SerializedProperty property, JToken value)
|
||||
{
|
||||
// Array/list fields (Generic + isArray) are written from a JSON array: resize, then recurse
|
||||
// per element so arrays of primitives AND object references (e.g. MeshRenderer.m_Materials)
|
||||
// both work. (String also reports isArray, hence the exclusion.)
|
||||
if (property.isArray && property.propertyType != SerializedPropertyType.String)
|
||||
{
|
||||
if (!(value is JArray array))
|
||||
throw new ArgumentException(
|
||||
$"Property '{property.name}' is an array; provide a JSON array of elements.");
|
||||
|
||||
property.arraySize = array.Count;
|
||||
for (int i = 0; i < array.Count; i++)
|
||||
Write(property.GetArrayElementAtIndex(i), array[i]);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (property.propertyType)
|
||||
{
|
||||
case SerializedPropertyType.Boolean:
|
||||
property.boolValue = value.ToObject<bool>();
|
||||
break;
|
||||
case SerializedPropertyType.Integer:
|
||||
property.longValue = value.ToObject<long>();
|
||||
break;
|
||||
case SerializedPropertyType.Float:
|
||||
property.doubleValue = value.ToObject<double>();
|
||||
break;
|
||||
case SerializedPropertyType.String:
|
||||
property.stringValue = value.Type == JTokenType.Null ? string.Empty : value.ToObject<string>();
|
||||
break;
|
||||
case SerializedPropertyType.Enum:
|
||||
WriteEnum(property, value);
|
||||
break;
|
||||
case SerializedPropertyType.Vector2:
|
||||
{
|
||||
var a = ToFloats(value, 2, property.name);
|
||||
property.vector2Value = new Vector2(a[0], a[1]);
|
||||
break;
|
||||
}
|
||||
case SerializedPropertyType.Vector3:
|
||||
{
|
||||
var a = ToFloats(value, 3, property.name);
|
||||
property.vector3Value = new Vector3(a[0], a[1], a[2]);
|
||||
break;
|
||||
}
|
||||
case SerializedPropertyType.Vector4:
|
||||
{
|
||||
var a = ToFloats(value, 4, property.name);
|
||||
property.vector4Value = new Vector4(a[0], a[1], a[2], a[3]);
|
||||
break;
|
||||
}
|
||||
case SerializedPropertyType.Color:
|
||||
{
|
||||
var a = ToFloats(value, 4, property.name);
|
||||
property.colorValue = new Color(a[0], a[1], a[2], a[3]);
|
||||
break;
|
||||
}
|
||||
case SerializedPropertyType.ObjectReference:
|
||||
property.objectReferenceValue = ResolveObjectReference(value, property.name);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException(
|
||||
$"Property '{property.name}' has unsupported type '{property.propertyType}' for set.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteEnum(SerializedProperty property, JToken value)
|
||||
{
|
||||
// Accept either the enum constant name or its index/value.
|
||||
if (value.Type == JTokenType.String)
|
||||
{
|
||||
var name = value.ToObject<string>();
|
||||
var index = Array.IndexOf(property.enumNames, name);
|
||||
if (index < 0)
|
||||
{
|
||||
// Fall back to display names (Editor humanized form), then error.
|
||||
index = Array.IndexOf(property.enumDisplayNames, name);
|
||||
if (index < 0)
|
||||
throw new ArgumentException(
|
||||
$"Enum value '{name}' is not valid for property '{property.name}'. Valid: [{string.Join(", ", property.enumNames)}].");
|
||||
}
|
||||
|
||||
property.enumValueIndex = index;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A numeric value is the enum's underlying integer, NOT an index into enumNames:
|
||||
// those only coincide when declaration order matches the constant values. Mirror Read,
|
||||
// which falls back to property.intValue, so round-tripping a numeric enum is stable.
|
||||
property.intValue = value.ToObject<int>();
|
||||
}
|
||||
}
|
||||
|
||||
private static Object ResolveObjectReference(JToken value, string propertyName)
|
||||
{
|
||||
// An explicit null clears the reference; anything else MUST resolve — never silently no-op
|
||||
// (a handle that doesn't resolve used to be dropped, so the command reported success while
|
||||
// assigning nothing).
|
||||
if (value == null || value.Type == JTokenType.Null)
|
||||
return null;
|
||||
|
||||
// Route every shape (string handle, handle object, or bare-integer instanceId) through
|
||||
// ObjectRefConverter so path / guid / globalId / instanceId forms all parse identically.
|
||||
var handle = value.ToObject<ObjectRef>();
|
||||
if (handle == null || handle.IsEmpty)
|
||||
throw new ArgumentException(
|
||||
$"Property '{propertyName}': '{value}' is not a recognized object reference handle. " +
|
||||
"Provide a path, guid, globalId, or instanceId (or null to clear).");
|
||||
|
||||
if (!ObjectResolver.TryResolve(handle, out var obj, out var error))
|
||||
throw new ArgumentException($"Could not resolve object reference for '{propertyName}': {error}");
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static float[] ToFloats(JToken value, int expected, string propertyName)
|
||||
{
|
||||
if (value is JArray array)
|
||||
{
|
||||
if (array.Count != expected)
|
||||
throw new ArgumentException(
|
||||
$"Property '{propertyName}' expects {expected} components, got {array.Count}.");
|
||||
var result = new float[expected];
|
||||
for (int i = 0; i < expected; i++)
|
||||
result[i] = array[i].ToObject<float>();
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new ArgumentException(
|
||||
$"Property '{propertyName}' expects an array of {expected} numbers.");
|
||||
}
|
||||
|
||||
private static JArray ToArray(params float[] values)
|
||||
{
|
||||
var array = new JArray();
|
||||
foreach (var v in values)
|
||||
array.Add(v);
|
||||
return array;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85f605c1109e40898fe47ff1e2a8a4d0
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a <see cref="UnityEngine.Component"/>-derived <see cref="Type"/> from an
|
||||
/// agent-supplied name. WHY this is non-trivial: agents pass either a short name ("Rigidbody")
|
||||
/// or a fully-qualified one ("UnityEngine.Camera"), and the type may live in any loaded assembly
|
||||
/// (UnityEngine modules, user assemblies, packages). We therefore try, in order: an exact
|
||||
/// fully-qualified <see cref="Type.GetType(string)"/>, then a scan of all loaded assemblies for a
|
||||
/// full-name match, then a short-name match. Only <see cref="Component"/> subclasses are accepted
|
||||
/// so the result is always valid for AddComponent/GetComponent.
|
||||
/// </summary>
|
||||
public static class TypeResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolve a component type by name, or return null if no unambiguous <see cref="Component"/>
|
||||
/// subclass matches. A short name that matches multiple types in different namespaces is
|
||||
/// treated as ambiguous and returns null (the caller should ask for a fully-qualified name).
|
||||
/// </summary>
|
||||
public static Type ResolveComponentType(string typeName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(typeName))
|
||||
return null;
|
||||
|
||||
typeName = typeName.Trim();
|
||||
|
||||
// 1. Exact, assembly-qualified or fully-qualified lookup.
|
||||
var direct = Type.GetType(typeName, throwOnError: false, ignoreCase: false);
|
||||
if (IsComponent(direct))
|
||||
return direct;
|
||||
|
||||
var assemblies = PipelineUtils.GetLoadedAssemblies();
|
||||
|
||||
// 2. Full-name match across loaded assemblies.
|
||||
var fullNameMatches = new List<Type>();
|
||||
// 3. Short-name match (collected in the same pass) as a fallback.
|
||||
var shortNameMatches = new List<Type>();
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (System.Reflection.ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types.Where(t => t != null).ToArray();
|
||||
}
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (!IsComponent(type))
|
||||
continue;
|
||||
|
||||
if (string.Equals(type.FullName, typeName, StringComparison.Ordinal))
|
||||
fullNameMatches.Add(type);
|
||||
else if (string.Equals(type.Name, typeName, StringComparison.Ordinal))
|
||||
shortNameMatches.Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
if (fullNameMatches.Count == 1)
|
||||
return fullNameMatches[0];
|
||||
if (fullNameMatches.Count > 1)
|
||||
return null; // genuinely ambiguous full names (rare) — caller must disambiguate.
|
||||
|
||||
// Distinct in case the same type is reachable through multiple assembly references.
|
||||
var distinctShort = shortNameMatches.Distinct().ToList();
|
||||
return distinctShort.Count == 1 ? distinctShort[0] : null;
|
||||
}
|
||||
|
||||
private static bool IsComponent(Type type)
|
||||
{
|
||||
return type != null && typeof(Component).IsAssignableFrom(type) && !type.IsAbstract;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f033287d429f4957bfcfd6795f731b48
|
||||
Reference in New Issue
Block a user