Files
AIC-Project/Client/PackageLocal/com.unity.pipeline@0.4.0-exp.1/Documentation~/authoring-commands.md
T

320 lines
17 KiB
Markdown

# Creating authoring commands
*Authoring commands* are the subset of commands that **create or mutate project content** — assets,
scenes, GameObjects, components — on behalf of an agent. They build on the base command API in
[Creating commands](creating-commands.md); this page covers the four concerns that are specific to
content authoring:
- **The authoring root** — the sandbox that bare paths resolve against and writes are confined to.
- **`ObjectRef`** — how a command *receives* a reference to an existing object.
- **`AuthoringResult`** — how a command *returns* the identity of an object it created or touched.
- **Undo/redo** — grouping a command's scene mutations into a single, revertible step.
Together these let one command's output feed the next command's input, so an agent can chain calls
(`create_gameobject``add_component``set_component_properties``create_prefab`) without ever
handling a raw Unity object itself. If you want to support a content type the package doesn't cover
yet (materials, audio, lighting, terrain, …), this is the pattern to follow.
Read [Creating commands](creating-commands.md) first — this page assumes you know how `[CliCommand]`,
`[CliArg]`, `MainThreadRequired`, and the response envelope work.
## Authoring vs. management commands
Not every state-changing command is an *authoring* command. This page — and the [checklist](#checklist-for-a-new-authoring-command)
at the end — applies to commands that **create or mutate project content** (assets, scenes,
GameObjects, components) and hand the agent back an object identity.
**Management commands** change *configuration* or *drive tooling* rather than author content:
project settings (`Editor/Commands/ProjectSettings/`), builds (`Build/`), and packages
(`PackageManager/`). They are a **different category** and deliberately do
**not** follow the full authoring contract. They share only the cross-cutting safety conventions
(the `confirm`/`dry_run` gate and, where relevant, `ObjectResolver`/`ProjectPaths`) — see
[Safety & mutations](safety-and-mutations.md).
| Concern | Authoring command | Management command |
|---|---|---|
| Object input | `ObjectRef``ObjectResolver.TryResolve` | same, when it references an object (e.g. a scene in the build list) |
| Path input | `ProjectPaths.Resolve` (confined to the authoring root) | as needed; may be unconfined by design (e.g. a build output path) |
| Undo | scene/object mutations wrapped in `AuthoringUndoScope` + registered `Undo` APIs | **none** — settings / AssetDatabase / UPM writes don't participate in Undo, so no scope; note `Not undoable via Ctrl+Z.` in the description instead |
| Destructive gate | `confirm`/`dry_run` where destructive | `confirm`/`dry_run` where destructive (same convention) |
| Return value | `AuthoringResult` via `ObjectResolver.Describe` | a domain model (e.g. `ProjectSettingsResponse`, `PackageMutationResponse`) or a plain result object |
| Failure | **throw** (`ArgumentException` / `InvalidOperationException`) | throw, **or** return a structured `{ success = false, code, error }` object — consistent within the area |
If you're adding a config/build/package command, follow the management column and the shared safety
conventions; the checklist below is for content-authoring commands.
## The building blocks
| Type | Namespace | Role |
|------|-----------|------|
| `ProjectPaths` | `Unity.Pipeline.Editor.Authoring` | Resolve/confine agent-supplied paths to the authoring root. |
| `ObjectRef` | `Unity.Pipeline.Models` | Input handle to an existing object (asset or scene object). |
| `AuthoringResult` | `Unity.Pipeline.Models` | Output identity of an object your command created or acted on. |
| `ObjectResolver` | `Unity.Pipeline.Editor.Authoring` | `TryResolve(ObjectRef …)` (handle → object) and `Describe(object)` (object → `AuthoringResult`). |
| `AuthoringUndoScope` | `Unity.Pipeline.Editor.Authoring` | Collapses all `Undo`-registered mutations in its lifetime into one editor undo step. |
Authoring commands are Editor-only. Put them under `Editor/Commands/<Area>/` in the
`Unity.Pipeline.Editor.Commands.<Area>` namespace (see `Editor/Commands/Authoring/AuthoringConfigCommands.cs`).
## The authoring root (`set_authoring_root`)
Agents pass **bare, relative paths** (`"Materials/Stone"`), not full project paths. `ProjectPaths`
resolves those against a configurable **authoring root** and *confines* every write to it. The root
defaults to `Assets` (full project access); an agent can narrow it to a sub-folder to sandbox itself:
```
get_authoring_root → { "root": "Assets" }
set_authoring_root --root Assets/AgentWork
```
These are just thin commands over `ProjectPaths.AuthoringRoot` (`Editor/Commands/Authoring/AuthoringConfigCommands.cs`):
```csharp
[CliCommand("set_authoring_root", "Set the base folder (under Assets/) that bare authoring paths resolve against and are confined to. Use 'Assets' for full project access.")]
public static object SetAuthoringRoot(
[CliArg("root", "Project-relative folder under Assets/, e.g. Assets/AgentWork. Use 'Assets' to allow the whole project.", Required = true)] string root)
{
// Throws ArgumentException for invalid roots (outside Assets/ or containing ".."); the
// server surfaces that message to the caller.
ProjectPaths.AuthoringRoot = root;
return new { root = ProjectPaths.AuthoringRoot };
}
```
**Every path parameter your command accepts must go through `ProjectPaths.Resolve`.** This is the
sandbox boundary — it rejects `..` traversal and anything that escapes the root, and it lets callers
omit the `Assets/` prefix. Do not build asset paths by string concatenation.
```csharp
var normalized = ProjectPaths.Resolve(path, out var error);
if (normalized == null)
throw new ArgumentException(error); // e.g. "Path '../secrets' must not contain '..'."
// normalized is now a project-relative path guaranteed to live under the authoring root.
```
Resolution rules (`Editor/Authoring/ProjectPaths.cs`):
- Bare paths (`"Materials/Stone"`) are taken relative to the root → `Assets/AgentWork/Materials/Stone`.
- Explicit `Assets/…` / `Packages/…` paths are used as-is (but still confined to the root).
- Absolute paths must live under the project root and are converted to project-relative.
- `..` anywhere, or a result outside the root, returns `null` + an `error` string.
## Receiving objects: `ObjectRef`
When a command needs to act on an object that *already* exists, take an `ObjectRef` parameter. The
agent supplies **one** of several forms; `ObjectResolver.TryResolve` tries them in order — `globalId`,
`path`, `guid` (+ optional `fileId`), `instanceId`, `hierarchyPath` — and hands you the live object:
```csharp
public static Renderer ResolveRenderer(ObjectRef target)
{
if (!ObjectResolver.TryResolve(target, out var obj, out var error))
throw new ArgumentException(error);
var go = obj as GameObject ?? (obj as Component)?.gameObject;
var renderer = go != null ? go.GetComponent<Renderer>() : null;
if (renderer == null)
throw new ArgumentException($"Object '{target}' has no Renderer.");
return renderer;
}
```
Resolve **outside** any undo scope / before mutating, so a bad handle fails before your command
changes anything (see `create_gameobject`, which resolves its `parent` before entering the scope).
When the handle is a plain string (the usual agent input), a value with a file extension and no
leading `/` — e.g. `"Materials/Floor.mat"` — is taken as an asset path and normalized under the
authoring root, so the `Assets/` prefix is optional (mirroring path-taking commands). A leading `/`
or an extension-less value stays a `hierarchyPath`; a dotted scene name like `"Cube.001"` still
resolves because the `path` branch falls back to a hierarchy lookup.
## Returning objects: `AuthoringResult`
Any command that creates or modifies an object should return its identity so the agent can reference
it in a follow-up call. Don't build this by hand — call `ObjectResolver.Describe(obj)`, which fills in
the right fields for the object kind:
- **Assets** get `assetPath`, `guid`, `fileId`, `type` (+ `globalId`).
- **Scene / loaded objects** get `instanceId`, `hierarchyPath`, `type` (+ `globalId`).
```csharp
var result = ObjectResolver.Describe(asset) ?? new AuthoringResult { Type = nameof(Material) };
result.AssetPath = assetPath; // ensure the path is set even if Describe returned a fresh result
return result;
```
`AuthoringResult` is **identity only** — no success flag, no message. Success/failure and timing live
in the outer `CommandExecutionResponse` (the server adds them). To report a failure, **throw**
(`ArgumentException` for bad input, `InvalidOperationException` for an operation that failed); the
server converts the exception into a failure envelope. For a batch, return a model that holds an
`AuthoringResult[]` (see `CreateGameObjectsResult`).
## Undo/redo
Undo is Unity's native `UnityEditor.Undo`, grouped per command by `AuthoringUndoScope` so a single
call reverts as a single Ctrl+Z step. Register each mutation with the matching `Undo` API **inside**
the scope:
```csharp
using (new AuthoringUndoScope("Set Material"))
{
Undo.RecordObject(renderer, "Set Material"); // record BEFORE mutating
renderer.sharedMaterial = material;
EditorSceneManager.MarkSceneDirty(renderer.gameObject.scene);
}
```
Which `Undo` call to use:
| Mutation | Call |
|----------|------|
| New scene object | `Undo.RegisterCreatedObjectUndo(obj, name)` |
| Change fields on an existing object | `Undo.RecordObject(obj, name)` / `RegisterCompleteObjectUndo` (before the change) |
| Add a component | `Undo.AddComponent(go, type)` |
| Reparent | `Undo.SetTransformParent(child, parent, name)` |
| Serialized properties | `SerializedObject` + `so.ApplyModifiedProperties()` (registers undo itself) |
> **Important caveat.** `AssetDatabase` operations are **not** part of Unity's undo system. Creating a
> folder or asset, importing, `AssetDatabase.CreateAsset`, `SaveAsPrefabAsset`, and file writes are
> **not undone** by Ctrl+Z. `AuthoringUndoScope` only covers scene/object mutations. If your command
> writes to disk, say so in its description and don't rely on undo to clean up — validate up front and
> fail before writing.
## Worked example — a new content type
Say the package has no material (rendering) commands and you want to add them. Two commands cover the
whole pattern: one that **creates an asset** (path handling + `AuthoringResult` out, no undo because
it's an `AssetDatabase` op) and one that **mutates a scene object** (`ObjectRef` in + undo).
```csharp
using System.IO;
using Unity.Pipeline.Commands;
using Unity.Pipeline.Editor.Authoring;
using Unity.Pipeline.Models;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace Unity.Pipeline.Editor.Commands.Rendering
{
/// <summary>Authoring commands for materials (rendering).</summary>
public static class MaterialCommands
{
[CliCommand("create_material",
"Create a Material asset (default shader 'Standard') under the authoring root. " +
"NOTE: asset creation is not undoable via Ctrl+Z.")]
public static AuthoringResult CreateMaterial(
[CliArg("path", "Asset path relative to the authoring root; the Assets/ prefix and the .mat extension are optional. e.g. Materials/Stone", Required = true)] string path,
[CliArg("shader", "Shader name to assign. Defaults to 'Standard'.")] string shader = "Standard")
{
// 1. Resolve + confine the path (the sandbox boundary).
var normalized = ProjectPaths.Resolve(path, out var error);
if (normalized == null)
throw new ArgumentException(error);
if (!normalized.EndsWith(".mat", System.StringComparison.OrdinalIgnoreCase))
normalized += ".mat";
var found = Shader.Find(shader);
if (found == null)
throw new ArgumentException($"Shader '{shader}' was not found.");
// 2. Do the work (AssetDatabase op — no undo scope; it wouldn't be undoable anyway).
EnsureParentFolder(normalized);
var material = new Material(found);
AssetDatabase.CreateAsset(material, normalized);
AssetDatabase.SaveAssets();
// 3. Return the created object's identity so the agent can reference it next.
var result = ObjectResolver.Describe(material) ?? new AuthoringResult { Type = nameof(Material) };
result.AssetPath = normalized;
return result;
}
[CliCommand("set_material", "Assign a material asset to a Renderer on a scene GameObject.")]
public static AuthoringResult SetMaterial(
[CliArg("target", "Handle of the GameObject (or Renderer) to modify.", Required = true)] ObjectRef target,
[CliArg("material", "Handle of the material asset to assign (path/guid/globalId).", Required = true)] ObjectRef material)
{
// Resolve both handles up front, before mutating, so a bad handle changes nothing.
var renderer = ResolveRenderer(target);
if (!ObjectResolver.TryResolve(material, out var matObj, out var matError))
throw new ArgumentException(matError);
if (matObj is not Material mat)
throw new ArgumentException($"'{material}' is not a Material.");
using (new AuthoringUndoScope("Set Material"))
{
Undo.RecordObject(renderer, "Set Material"); // record BEFORE the change
renderer.sharedMaterial = mat;
EditorSceneManager.MarkSceneDirty(renderer.gameObject.scene);
}
return ObjectResolver.Describe(renderer);
}
private static Renderer ResolveRenderer(ObjectRef target)
{
if (!ObjectResolver.TryResolve(target, out var obj, out var error))
throw new ArgumentException(error);
var go = obj as GameObject ?? (obj as Component)?.gameObject;
var renderer = go != null ? go.GetComponent<Renderer>() : null;
if (renderer == null)
throw new ArgumentException($"Object '{target}' has no Renderer.");
return renderer;
}
// Mirrors the shared helper used by the built-in asset commands.
private static void EnsureParentFolder(string assetPath)
{
var parent = Path.GetDirectoryName(assetPath)?.Replace('\\', '/');
if (string.IsNullOrEmpty(parent) || AssetDatabase.IsValidFolder(parent))
return;
// Create intermediate folders (see create_folder / CreateFolderRecursive).
Directory.CreateDirectory(ProjectPaths.ProjectRoot + "/" + parent);
AssetDatabase.Refresh();
}
}
}
```
An agent chains them by feeding the first result into the second:
```
create_material --path Materials/Stone --shader Standard
→ { "assetPath": "Assets/AgentWork/Materials/Stone.mat", "guid": "…", "type": "Material" }
set_material --target '{"hierarchyPath":"/Ground"}' --material '{"path":"Assets/AgentWork/Materials/Stone.mat"}'
→ { "instanceId": …, "hierarchyPath": "/Ground", "type": "MeshRenderer" }
```
## Checklist for a new authoring command
For *content-authoring* commands. Config/build/package commands follow the lighter
[management-command conventions](#authoring-vs-management-commands) instead.
- [ ] Editor-only, under `Editor/Commands/<Area>/`, `static` (any accessibility — `public`/`internal`/`private`), tagged `[CliCommand]`.
- [ ] Every path parameter resolved through `ProjectPaths.Resolve` (never concatenated).
- [ ] Existing-object inputs taken as `ObjectRef`, resolved via `ObjectResolver.TryResolve`; resolve **before** mutating.
- [ ] Scene/object mutations wrapped in an `AuthoringUndoScope` and registered with the matching `Undo` API.
- [ ] `AssetDatabase` writes noted as non-undoable in the description; validate before writing.
- [ ] Returns `AuthoringResult` (via `ObjectResolver.Describe`) or a model containing `AuthoringResult[]`.
- [ ] Reports failures by throwing; never build the response envelope yourself.
## Build & verify
New commands register automatically after the next recompile (see
[Creating commands → Discovery](creating-commands.md#discovery)). Drive the live editor to verify:
```
command recompile
command recompile_status # poll until done
command create_material --path Materials/Stone
```
## See also
- [Creating commands](creating-commands.md) — the base command API this page builds on.
- [Asset & file commands](commands/assets-and-files.md) — reference for the built-in asset commands.
- [GameObject & component commands](commands/gameobjects-and-components.md) — the scene-mutation commands.