Add Unity Pipeline package support
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Groups all Undo operations registered during its lifetime into a single, collapsible Editor
|
||||
/// Undo step, so a multi-step agent action reverts as one. Register mutations inside the scope
|
||||
/// with Undo.RegisterCreatedObjectUndo / RegisterCompleteObjectUndo / etc.
|
||||
/// </para>
|
||||
///
|
||||
/// <code>
|
||||
/// using (new AuthoringUndoScope("Create Enemy"))
|
||||
/// {
|
||||
/// var go = new GameObject("Enemy");
|
||||
/// Undo.RegisterCreatedObjectUndo(go, "Create Enemy");
|
||||
/// // ... further registered mutations collapse into the same step
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// <para>
|
||||
/// NOTE: minimal seed for the shared safety policy (CAT-2509). AssetDatabase operations
|
||||
/// (folder/asset creation, import) are NOT part of Unity's Undo system, so this scope only
|
||||
/// affects scene/object mutations.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class AuthoringUndoScope : IDisposable
|
||||
{
|
||||
private readonly int m_Group;
|
||||
|
||||
public AuthoringUndoScope(string name)
|
||||
{
|
||||
Undo.IncrementCurrentGroup();
|
||||
m_Group = Undo.GetCurrentGroup();
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
Undo.SetCurrentGroupName(name);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Undo.CollapseUndoOperations(m_Group);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ef72626627670a4eadfccdab20cd9ba
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves an agent-supplied <see cref="ObjectRef"/> handle to a live UnityEngine.Object, and
|
||||
/// describes any object back into a canonical <see cref="AuthoringResult"/> identity.
|
||||
/// Backed by GlobalObjectId, which addresses both assets (GUID + fileId) and scene objects.
|
||||
/// </summary>
|
||||
public static class ObjectResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Try to resolve a handle to a loaded object. Returns false with an <paramref name="error"/>
|
||||
/// when the handle is empty or does not resolve.
|
||||
/// </summary>
|
||||
public static bool TryResolve(ObjectRef handle, out Object obj, out string error)
|
||||
{
|
||||
obj = null;
|
||||
error = null;
|
||||
|
||||
if (handle == null || handle.IsEmpty)
|
||||
{
|
||||
error = "Empty object reference.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Canonical GlobalObjectId.
|
||||
if (!string.IsNullOrEmpty(handle.GlobalId))
|
||||
{
|
||||
if (!GlobalObjectId.TryParse(handle.GlobalId, out var gid))
|
||||
{
|
||||
error = $"Invalid globalId '{handle.GlobalId}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
obj = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(gid);
|
||||
if (obj != null)
|
||||
return true;
|
||||
|
||||
error = $"globalId '{handle.GlobalId}' did not resolve to a loaded object.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Asset path. Explicit "Assets/"/"Packages/"-rooted paths load as-is; a bare relative
|
||||
// path (e.g. "Materials/Floor.mat") is normalized under the authoring root first. If it
|
||||
// does not resolve to an asset, fall back to a hierarchy lookup so dotted GameObject names
|
||||
// (e.g. "Cube.001") still resolve, and report every strategy that was tried.
|
||||
if (!string.IsNullOrEmpty(handle.Path))
|
||||
{
|
||||
var raw = handle.Path;
|
||||
var explicitlyRooted = IsExplicitlyRooted(raw);
|
||||
var assetPath = raw;
|
||||
if (!explicitlyRooted)
|
||||
{
|
||||
var resolved = ProjectPaths.Resolve(raw, out _);
|
||||
if (!string.IsNullOrEmpty(resolved))
|
||||
assetPath = resolved;
|
||||
}
|
||||
|
||||
obj = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
||||
if (obj != null)
|
||||
return true;
|
||||
|
||||
if (explicitlyRooted)
|
||||
{
|
||||
error = $"No asset at path '{raw}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// A bare relative string may actually be a scene hierarchy path with a dotted name.
|
||||
var go = FindByHierarchyPath(raw);
|
||||
if (go != null)
|
||||
{
|
||||
obj = go;
|
||||
return true;
|
||||
}
|
||||
|
||||
error = $"Could not resolve '{raw}': no asset at '{assetPath}', no GameObject at hierarchy path '{raw}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. GUID (+ optional fileId for sub-assets).
|
||||
if (!string.IsNullOrEmpty(handle.Guid))
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(handle.Guid);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
error = $"Unknown GUID '{handle.Guid}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (handle.FileId.HasValue)
|
||||
{
|
||||
foreach (var candidate in AssetDatabase.LoadAllAssetsAtPath(path))
|
||||
{
|
||||
if (candidate != null &&
|
||||
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(candidate, out _, out long localId) &&
|
||||
localId == handle.FileId.Value)
|
||||
{
|
||||
obj = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
error = $"No sub-asset with fileId {handle.FileId} in '{path}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
obj = AssetDatabase.LoadMainAssetAtPath(path);
|
||||
if (obj != null)
|
||||
return true;
|
||||
|
||||
error = $"Could not load asset '{path}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. Instance id (loaded/scene object).
|
||||
if (handle.InstanceId.HasValue)
|
||||
{
|
||||
obj = PipelineUtils.IdToObject(handle.InstanceId.Value);
|
||||
if (obj != null)
|
||||
return true;
|
||||
|
||||
error = $"No loaded object with instanceId {handle.InstanceId}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. Scene hierarchy path.
|
||||
if (!string.IsNullOrEmpty(handle.HierarchyPath))
|
||||
{
|
||||
var go = FindByHierarchyPath(handle.HierarchyPath);
|
||||
if (go != null)
|
||||
{
|
||||
obj = go;
|
||||
return true;
|
||||
}
|
||||
|
||||
error = $"No GameObject at hierarchy path '{handle.HierarchyPath}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = "Empty object reference.";
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Produce the canonical identity for an object (assets get path/guid/fileId; loaded objects
|
||||
/// get instanceId/hierarchyPath). Returns null for a null object.
|
||||
/// </summary>
|
||||
public static AuthoringResult Describe(Object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return null;
|
||||
|
||||
var result = new AuthoringResult { Type = obj.GetType().Name };
|
||||
|
||||
var assetPath = AssetDatabase.GetAssetPath(obj);
|
||||
if (!string.IsNullOrEmpty(assetPath))
|
||||
{
|
||||
result.AssetPath = assetPath;
|
||||
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long localId))
|
||||
{
|
||||
result.Guid = guid;
|
||||
result.FileId = localId;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.InstanceId = PipelineUtils.GetObjectId(obj);
|
||||
var go = obj as GameObject ?? (obj as Component)?.gameObject;
|
||||
if (go != null)
|
||||
result.HierarchyPath = GetHierarchyPath(go);
|
||||
}
|
||||
|
||||
// Not every object has a global id (e.g. transient objects); ignore failures.
|
||||
try
|
||||
{
|
||||
result.GlobalId = GlobalObjectId.GetGlobalObjectIdSlow(obj).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// leave GlobalId null
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>True when a path is explicitly rooted at "Assets/" or "Packages/" (or is exactly one of those).</summary>
|
||||
private static bool IsExplicitlyRooted(string path) =>
|
||||
path == "Assets" || path == "Packages"
|
||||
|| path.StartsWith("Assets/", System.StringComparison.Ordinal)
|
||||
|| path.StartsWith("Packages/", System.StringComparison.Ordinal);
|
||||
|
||||
private static GameObject FindByHierarchyPath(string path)
|
||||
{
|
||||
var parts = path.Trim('/').Split('/');
|
||||
if (parts.Length == 0 || string.IsNullOrEmpty(parts[0]))
|
||||
return null;
|
||||
|
||||
for (int s = 0; s < SceneManager.sceneCount; s++)
|
||||
{
|
||||
var scene = SceneManager.GetSceneAt(s);
|
||||
if (!scene.isLoaded)
|
||||
continue;
|
||||
|
||||
foreach (var root in scene.GetRootGameObjects())
|
||||
{
|
||||
if (root.name != parts[0])
|
||||
continue;
|
||||
|
||||
var current = root.transform;
|
||||
var matched = true;
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
var child = current.Find(parts[i]);
|
||||
if (child == null)
|
||||
{
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
|
||||
current = child;
|
||||
}
|
||||
|
||||
if (matched)
|
||||
return current.gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26becd8c305c973488fa15b9ea058a00
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Project-path handling for authoring commands.
|
||||
///
|
||||
/// - Paths resolve relative to the configurable <see cref="AuthoringRoot"/> (default "Assets"),
|
||||
/// so callers can pass bare paths ("Gameplay/Enemies") without repeating the "Assets/" prefix.
|
||||
/// - Explicit "Assets/..." (or "Packages/...") paths are treated as project-relative as-is.
|
||||
/// - Every resolved path is confined to <see cref="AuthoringRoot"/> (the sandbox). With the
|
||||
/// default root ("Assets") that means full Assets access; set a sub-folder to confine an agent.
|
||||
/// - "../" traversal and writes outside the project root are always rejected.
|
||||
///
|
||||
/// NOTE: minimal seed for the shared safety policy (CAT-2509).
|
||||
/// </summary>
|
||||
public static class ProjectPaths
|
||||
{
|
||||
private const string DefaultRoot = "Assets";
|
||||
|
||||
// Per-project EditorPrefs key so the root doesn't leak across projects on the same machine.
|
||||
private static string PrefKey => $"Unity.Pipeline.AuthoringRoot:{Application.dataPath}";
|
||||
|
||||
/// <summary>Absolute path to the project root (the folder that contains Assets/).</summary>
|
||||
public static string ProjectRoot => Path.GetDirectoryName(Application.dataPath);
|
||||
|
||||
/// <summary>
|
||||
/// Base folder (project-relative, under Assets/) that bare paths resolve against and that all
|
||||
/// authoring writes are confined to. Defaults to "Assets". Persisted per project via EditorPrefs.
|
||||
/// Setting an invalid value (outside Assets/ or containing "..") throws.
|
||||
/// </summary>
|
||||
public static string AuthoringRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
var root = EditorPrefs.GetString(PrefKey, DefaultRoot);
|
||||
return string.IsNullOrEmpty(root) ? DefaultRoot : root;
|
||||
}
|
||||
set
|
||||
{
|
||||
var normalized = NormalizeRoot(value, out var error);
|
||||
if (normalized == null)
|
||||
throw new ArgumentException(error);
|
||||
EditorPrefs.SetString(PrefKey, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reset the authoring root to the default ("Assets").</summary>
|
||||
public static void ResetAuthoringRoot() => EditorPrefs.DeleteKey(PrefKey);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve an agent-supplied path to a normalized, project-relative path confined to
|
||||
/// <see cref="AuthoringRoot"/>. Bare paths are taken relative to the root; explicit
|
||||
/// "Assets/..." paths and absolute paths under the project are honored. Returns null with an
|
||||
/// <paramref name="error"/> when the path escapes the root or the project.
|
||||
/// </summary>
|
||||
public static string Resolve(string path, out string error)
|
||||
{
|
||||
error = null;
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
error = "Path is required.";
|
||||
return null;
|
||||
}
|
||||
|
||||
var root = AuthoringRoot;
|
||||
var normalized = path.Replace('\\', '/').Trim();
|
||||
|
||||
// Absolute path: must live under the project root; convert to project-relative.
|
||||
if (Path.IsPathRooted(normalized))
|
||||
{
|
||||
var full = Path.GetFullPath(normalized).Replace('\\', '/');
|
||||
var projectRoot = ProjectRoot.Replace('\\', '/').TrimEnd('/');
|
||||
if (!full.Equals(projectRoot, StringComparison.OrdinalIgnoreCase) &&
|
||||
!full.StartsWith(projectRoot + "/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = $"Path '{path}' is outside the project root '{projectRoot}'.";
|
||||
return null;
|
||||
}
|
||||
|
||||
normalized = full.Length > projectRoot.Length ? full.Substring(projectRoot.Length + 1) : string.Empty;
|
||||
}
|
||||
|
||||
normalized = normalized.TrimStart('/');
|
||||
|
||||
if (normalized.Split('/').Contains(".."))
|
||||
{
|
||||
error = $"Path '{path}' must not contain '..'.";
|
||||
return null;
|
||||
}
|
||||
|
||||
// Explicit project-relative paths ("Assets/..." / "Packages/...") are used as-is; anything
|
||||
// else is a bare path taken relative to the authoring root.
|
||||
var isProjectRelative =
|
||||
normalized == "Assets" || normalized.StartsWith("Assets/", StringComparison.Ordinal) ||
|
||||
normalized == "Packages" || normalized.StartsWith("Packages/", StringComparison.Ordinal);
|
||||
|
||||
var resolved = (isProjectRelative ? normalized : CombineUnder(root, normalized)).TrimEnd('/');
|
||||
|
||||
// Confine to the authoring root (default "Assets" => full Assets access).
|
||||
if (!(resolved == root || resolved.StartsWith(root + "/", StringComparison.Ordinal)))
|
||||
{
|
||||
error = $"Path '{path}' resolves to '{resolved}', which is outside the authoring root '{root}'.";
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static string CombineUnder(string root, string relative)
|
||||
{
|
||||
relative = relative.TrimStart('/');
|
||||
return string.IsNullOrEmpty(relative) ? root : $"{root}/{relative}";
|
||||
}
|
||||
|
||||
private static string NormalizeRoot(string value, out string error)
|
||||
{
|
||||
error = null;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
error = "Authoring root is required.";
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = value.Replace('\\', '/').Trim().TrimStart('/').TrimEnd('/');
|
||||
if (normalized.Split('/').Contains(".."))
|
||||
{
|
||||
error = $"Authoring root '{value}' must not contain '..'.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(normalized == "Assets" || normalized.StartsWith("Assets/", StringComparison.Ordinal)))
|
||||
{
|
||||
error = $"Authoring root '{value}' must be under the project's Assets/ folder.";
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 462c39b6cec1060408ca75a32bf31d89
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user