using System;
using Unity.Pipeline.Commands;
using Unity.Pipeline.Editor.Authoring;
using Unity.Pipeline.Models;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Unity.Pipeline.Editor.Commands.Scripts
{
///
/// Authoring command that adds a MonoBehaviour component to a GameObject, addressing the script
/// either by its (compiled) type name OR by its source asset path (CLI-195, CLI-224).
///
/// COMPILE-AWARE: the type must already be compiled. A script created via create_script does not
/// have a compiled until a domain reload runs, so attaching it before that
/// fails with a CLEAR, RECOVERABLE error (no crash, no silent no-op) telling the agent to
/// recompile and retry. The full flow is:
/// create_script -> recompile -> poll recompile_status (completed/up_to_date) -> attach_script.
///
/// Addressing by asset path resolves the backing class through
/// , which correctly handles a class whose name differs from
/// the filename. A path whose MonoScript has no class yet (not compiled, or no single matching
/// top-level type) surfaces the same recoverable "not yet compiled / ambiguous" error.
///
/// The component add is wrapped in an and registered with the
/// Undo system so it reverts as one step and the owning object/prefab is marked dirty.
///
public static class AttachScriptCommand
{
[CliCommand("attach_script",
"Add a MonoBehaviour to a GameObject by its (compiled) type name OR by its script asset path. " +
"Provide exactly one of 'type' or 'script'. " +
"If the type isn't compiled yet, returns a recoverable error: recompile, poll recompile_status, then retry.")]
public static AuthoringResult AttachScript(
[CliArg("target", "Reference to the GameObject to add the component to (globalId/path/guid/instanceId/hierarchyPath).", Required = true)] ObjectRef target,
[CliArg("type", "Component type name to add, e.g. PlayerController or Game.Player.PlayerController. Must already be compiled. Mutually exclusive with 'script'.")] string type = null,
[CliArg("script", "Script asset path, e.g. 'Assets/Pool/Scripts/CueShooter.cs'. The backing class is resolved via MonoScript.GetClass(), so the class name may differ from the filename. Mutually exclusive with 'type'.")] string script = null)
{
if (target == null || target.IsEmpty)
throw new ArgumentException("attach_script 'target' is required.");
// Exactly one addressing form must be supplied. Both is ambiguous; neither is incomplete.
var hasType = !string.IsNullOrWhiteSpace(type);
var hasScript = !string.IsNullOrWhiteSpace(script);
if (hasType && hasScript)
throw new ArgumentException(
"Provide either 'type' (class name) or 'script' (asset path), not both.");
if (!hasType && !hasScript)
throw new ArgumentException(
"Provide either 'type' (class name) or 'script' (asset path).");
if (!ObjectResolver.TryResolve(target, out var obj, out var resolveError))
throw new ArgumentException($"Could not resolve target: {resolveError}");
var go = obj as GameObject ?? (obj as Component)?.gameObject;
if (go == null)
throw new ArgumentException(
$"Target '{target}' resolved to a {obj.GetType().Name}, which is not a GameObject. " +
"attach_script needs a GameObject.");
// Resolve the component Type from whichever addressing form was given. A type that exists but
// isn't compiled yet (or is the wrong kind) surfaces as InvalidOperationException so the
// server returns a 400 "Command Execution Failed" with the recovery instructions intact.
// ArgumentException is reserved for caller input mistakes — both/neither args, or a script
// path that points at no asset — which the server reports as "Parameter Validation Failed".
var componentType = hasScript
? ResolveTypeFromScriptPath(script)
: ResolveTypeFromName(type);
using (new AuthoringUndoScope($"Attach {componentType.Name}"))
{
// Undo.AddComponent registers the add for undo and returns the new component.
var component = Undo.AddComponent(go, componentType);
if (component == null)
throw new InvalidOperationException(
$"Failed to add component '{componentType.FullName}' to '{go.name}'. " +
"It may conflict with [DisallowMultipleComponent] or a required-component rule.");
EditorUtility.SetDirty(go);
var result = ObjectResolver.Describe(component) ?? new AuthoringResult { Type = componentType.Name };
return result;
}
}
///
/// Resolve a component type by its compiled type name. A missing type is the expected,
/// recoverable "created but not yet compiled" case.
///
private static Type ResolveTypeFromName(string type)
{
if (!ScriptTypeResolver.TryResolveComponentType(type, out var componentType, out var typeError))
throw new InvalidOperationException(typeError);
return componentType;
}
///
/// Resolve a component type from a script asset path. Loads the at the
/// path and resolves its backing class via — which correctly
/// handles a class whose name differs from the filename. Validates the class is a concrete
/// MonoBehaviour. A null class is the recoverable "not yet compiled / ambiguous" case and
/// mirrors the type-not-found message style. A path that resolves to no MonoScript asset is a
/// caller input mistake (ArgumentException → "Parameter Validation Failed"), not a recompile-
/// recoverable failure.
///
private static Type ResolveTypeFromScriptPath(string path)
{
var trimmed = path.Trim();
var mono = AssetDatabase.LoadAssetAtPath(trimmed);
if (mono == null)
throw new ArgumentException(
$"No MonoScript at '{trimmed}'. Check the asset path (it should be a project-relative " +
"path to a .cs file, e.g. 'Assets/Scripts/PlayerController.cs').");
var cls = mono.GetClass();
if (cls == null)
throw new InvalidOperationException(
$"Script at '{trimmed}' has no resolvable class. " +
"If you just created or edited this script it is not compiled yet: run 'recompile', poll " +
"'recompile_status' until it reports completed/up_to_date, then retry attach_script. " +
"If it should already exist, ensure the file has a single top-level type matching the " +
"script and that it compiled without errors.");
if (!typeof(MonoBehaviour).IsAssignableFrom(cls))
throw new InvalidOperationException(
$"Type '{cls.FullName}' (from '{trimmed}') does not derive from MonoBehaviour and cannot be added as a component.");
if (cls.IsAbstract)
throw new InvalidOperationException(
$"Type '{cls.FullName}' (from '{trimmed}') is abstract and cannot be instantiated as a component.");
return cls;
}
}
}