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; } } }