320 lines
17 KiB
Markdown
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.
|