Add Unity Pipeline package support
This commit is contained in:
+602
@@ -0,0 +1,602 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Unity.Pipeline.Commands;
|
||||
using Unity.Pipeline.Editor.Authoring;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
using PipelinePhysicsMaterial = UnityEngine.PhysicsMaterial;
|
||||
#else
|
||||
using PipelinePhysicsMaterial = UnityEngine.PhysicMaterial;
|
||||
#endif
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Asset lifecycle authoring commands (CLI-191): create / import / move / copy / rename / delete
|
||||
/// project assets. They sit on top of the CLI-190 authoring foundation, so:
|
||||
///
|
||||
/// - every agent-supplied path is funnelled through <see cref="ProjectPaths.Resolve"/> (sandboxed
|
||||
/// to the authoring root; rejects "../" and out-of-project writes),
|
||||
/// - results are returned as the canonical <see cref="AuthoringResult"/> envelope so an agent can
|
||||
/// reference the asset in a follow-up call,
|
||||
/// - existing assets are addressed with an <see cref="ObjectRef"/> resolved by
|
||||
/// <see cref="ObjectResolver"/>.
|
||||
///
|
||||
/// NOTE: AssetDatabase create/move/copy/rename/delete are NOT part of Unity's Undo system, so
|
||||
/// <see cref="AuthoringUndoScope"/> would have no effect here — these operations are intentionally
|
||||
/// not wrapped in an undo scope, and destructive/overwriting operations instead require an explicit
|
||||
/// <c>confirm</c> argument (and support <c>dry_run</c>) so an agent cannot silently lose data.
|
||||
/// </summary>
|
||||
public static class AssetCommands
|
||||
{
|
||||
[CliCommand("create_asset", "Create a new ScriptableObject (or other UnityEngine.Object) asset of the given type at a path under the authoring root.")]
|
||||
public static AuthoringResult CreateAsset(
|
||||
[CliArg("path", "Asset path relative to the authoring root, including extension (e.g. Data/Config.asset or Materials/Wall.mat). The Assets/ prefix is optional.", Required = true)] string path,
|
||||
[CliArg("type", "Fully-qualified or short type name to instantiate (e.g. UnityEngine.Material, MyGame.GameConfig). Must derive from UnityEngine.Object and be creatable.", Required = true)] string type,
|
||||
[CliArg("shader", "Material-only (ignored otherwise): shader name to assign (e.g. Standard, \"Universal Render Pipeline/Lit\"). When omitted, defaults to \"Universal Render Pipeline/Lit\" if a Scriptable Render Pipeline is active, otherwise the built-in \"Standard\" shader (falling back to \"Standard\" if URP/Lit is unavailable).")] string shader = null,
|
||||
[CliArg("confirm", "Required (true) only when overwriting an existing asset at the path. Ignored when the path is empty.")] bool confirm = false,
|
||||
[CliArg("dry_run", "If true, validate inputs and report what would be created without writing anything.")] bool dryRun = false)
|
||||
{
|
||||
var normalized = ProjectPaths.Resolve(path, out var error);
|
||||
if (normalized == null)
|
||||
throw new ArgumentException(error);
|
||||
|
||||
if (string.IsNullOrEmpty(Path.GetExtension(normalized)))
|
||||
throw new ArgumentException($"Asset path '{normalized}' must include a file extension (e.g. .asset, .mat).");
|
||||
|
||||
var resolvedType = ResolveType(type);
|
||||
if (resolvedType == null)
|
||||
throw new ArgumentException($"Could not resolve type '{type}'. Use a fully-qualified name (e.g. UnityEngine.Material).");
|
||||
if (!typeof(Object).IsAssignableFrom(resolvedType))
|
||||
throw new ArgumentException($"Type '{resolvedType.FullName}' does not derive from UnityEngine.Object.");
|
||||
|
||||
// CLI-222: Unity 6 renamed PhysicMaterial -> PhysicsMaterial, but the on-disk asset
|
||||
// extension is still ".physicMaterial". AssetDatabase.CreateAsset rejects a
|
||||
// ".physicsMaterial" file ("should not be used to create a file of type 'physicsMaterial'"),
|
||||
// so accept either spelling from the agent and normalize to the extension Unity writes.
|
||||
if (typeof(PipelinePhysicsMaterial).IsAssignableFrom(resolvedType) &&
|
||||
normalized.EndsWith(".physicsMaterial", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalized = normalized.Substring(0, normalized.Length - ".physicsMaterial".Length) + ".physicMaterial";
|
||||
}
|
||||
|
||||
var exists = AssetDatabase.LoadMainAssetAtPath(normalized) != null;
|
||||
if (exists && !confirm)
|
||||
throw new ArgumentException($"An asset already exists at '{normalized}'. Pass confirm=true to overwrite it.");
|
||||
|
||||
// CLI-221: for a Material, resolve (and thereby validate) the shader up front so dry_run
|
||||
// also fails fast on an unknown shader — dry_run is documented as "validate inputs".
|
||||
Shader materialShader = null;
|
||||
if (typeof(Material).IsAssignableFrom(resolvedType))
|
||||
materialShader = ResolveShader(shader);
|
||||
|
||||
if (dryRun)
|
||||
return new AuthoringResult { AssetPath = normalized, Type = resolvedType.Name };
|
||||
|
||||
EnsureParentFolder(normalized);
|
||||
|
||||
Object asset;
|
||||
if (typeof(ScriptableObject).IsAssignableFrom(resolvedType))
|
||||
{
|
||||
asset = ScriptableObject.CreateInstance(resolvedType);
|
||||
}
|
||||
else if (typeof(Material).IsAssignableFrom(resolvedType))
|
||||
{
|
||||
// CLI-221: Material has no public parameterless ctor (Activator.CreateInstance produces
|
||||
// an invalid Material with a null shader), so construct it explicitly from the shader
|
||||
// resolved (and validated) above. The `shader` arg is Material-specific and ignored for
|
||||
// every other type.
|
||||
asset = new Material(materialShader);
|
||||
}
|
||||
else if (typeof(PipelinePhysicsMaterial).IsAssignableFrom(resolvedType))
|
||||
{
|
||||
// CLI-222: PhysicsMaterial has a public parameterless ctor, but create it explicitly so
|
||||
// we can stamp the documented sensible defaults rather than relying on engine defaults.
|
||||
asset = new PipelinePhysicsMaterial
|
||||
{
|
||||
dynamicFriction = 0.6f,
|
||||
staticFriction = 0.6f,
|
||||
bounciness = 0f,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Other UnityEngine.Object subclasses with a public parameterless ctor.
|
||||
// Activator.CreateInstance throws for abstract types or types without an accessible
|
||||
// parameterless ctor — surface that as an actionable ArgumentException.
|
||||
try
|
||||
{
|
||||
asset = Activator.CreateInstance(resolvedType) as Object;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Type '{resolvedType.FullName}' could not be instantiated (it may be abstract or lack a public parameterless constructor): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (asset == null)
|
||||
throw new ArgumentException($"Type '{resolvedType.FullName}' could not be instantiated as a creatable asset.");
|
||||
|
||||
// AssetDatabase.CreateAsset does not reliably overwrite an existing asset at the path, so
|
||||
// explicitly delete it first. The confirm guard above gates that an asset already exists.
|
||||
if (exists)
|
||||
AssetDatabase.DeleteAsset(normalized);
|
||||
|
||||
AssetDatabase.CreateAsset(asset, normalized);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.ImportAsset(normalized);
|
||||
|
||||
var loaded = AssetDatabase.LoadMainAssetAtPath(normalized);
|
||||
var result = ObjectResolver.Describe(loaded) ?? new AuthoringResult { Type = resolvedType.Name };
|
||||
result.AssetPath = normalized;
|
||||
return result;
|
||||
}
|
||||
|
||||
[CliCommand("import_asset", "Import an external file (e.g. a texture, model, audio clip) into the project by copying it to a path under the authoring root, then importing it.")]
|
||||
public static AuthoringResult ImportAsset(
|
||||
[CliArg("source", "Absolute filesystem path to the external file to import.", Required = true)] string source,
|
||||
[CliArg("path", "Destination asset path relative to the authoring root, including extension. The Assets/ prefix is optional.", Required = true)] string path,
|
||||
[CliArg("confirm", "Required (true) only when overwriting an existing asset at the destination path.")] bool confirm = false,
|
||||
[CliArg("dry_run", "If true, validate inputs and report what would be imported without writing anything.")] bool dryRun = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
throw new ArgumentException("source is required.");
|
||||
if (!File.Exists(source))
|
||||
throw new ArgumentException($"Source file '{source}' does not exist.");
|
||||
|
||||
var normalized = ProjectPaths.Resolve(path, out var error);
|
||||
if (normalized == null)
|
||||
throw new ArgumentException(error);
|
||||
|
||||
var exists = AssetDatabase.LoadMainAssetAtPath(normalized) != null || File.Exists(ToAbsolute(normalized));
|
||||
if (exists && !confirm)
|
||||
throw new ArgumentException($"An asset already exists at '{normalized}'. Pass confirm=true to overwrite it.");
|
||||
|
||||
if (dryRun)
|
||||
return new AuthoringResult { AssetPath = normalized, Type = "ImportedAsset" };
|
||||
|
||||
EnsureParentFolder(normalized);
|
||||
File.Copy(source, ToAbsolute(normalized), overwrite: true);
|
||||
AssetDatabase.ImportAsset(normalized, ImportAssetOptions.ForceUpdate);
|
||||
|
||||
var loaded = AssetDatabase.LoadMainAssetAtPath(normalized);
|
||||
var result = ObjectResolver.Describe(loaded) ?? new AuthoringResult();
|
||||
result.AssetPath = normalized;
|
||||
return result;
|
||||
}
|
||||
|
||||
[CliCommand("move_asset", "Move (or rename via a new path) an asset to a new location under the authoring root. Preserves the asset's GUID.")]
|
||||
public static AuthoringResult MoveAsset(
|
||||
[CliArg("asset", "Reference to the asset to move (path / guid / globalId).", Required = true)] ObjectRef asset,
|
||||
[CliArg("destination", "Destination asset path relative to the authoring root, including extension. The Assets/ prefix is optional.", Required = true)] string destination,
|
||||
[CliArg("dry_run", "If true, validate the move (via AssetDatabase.ValidateMoveAsset) without performing it.")] bool dryRun = false)
|
||||
{
|
||||
var sourcePath = ResolveAssetPath(asset);
|
||||
|
||||
var dest = ProjectPaths.Resolve(destination, out var error);
|
||||
if (dest == null)
|
||||
throw new ArgumentException(error);
|
||||
|
||||
// For a real move, create the destination's parent folder *before* validating: Unity's
|
||||
// ValidateMoveAsset reports failure ("move as a subdirectory") when the target folder
|
||||
// doesn't exist yet. Dry-run must not create folders, so it validates against the current
|
||||
// project state only.
|
||||
if (!dryRun)
|
||||
EnsureParentFolder(dest);
|
||||
|
||||
var validation = AssetDatabase.ValidateMoveAsset(sourcePath, dest);
|
||||
if (!string.IsNullOrEmpty(validation))
|
||||
throw new ArgumentException($"Cannot move '{sourcePath}' to '{dest}': {validation}");
|
||||
|
||||
if (dryRun)
|
||||
return new AuthoringResult { AssetPath = dest, Type = AssetDatabase.GetMainAssetTypeAtPath(sourcePath)?.Name };
|
||||
|
||||
var moveError = AssetDatabase.MoveAsset(sourcePath, dest);
|
||||
if (!string.IsNullOrEmpty(moveError))
|
||||
throw new ArgumentException($"Failed to move '{sourcePath}' to '{dest}': {moveError}");
|
||||
|
||||
var loaded = AssetDatabase.LoadMainAssetAtPath(dest);
|
||||
var result = ObjectResolver.Describe(loaded) ?? new AuthoringResult();
|
||||
result.AssetPath = dest;
|
||||
return result;
|
||||
}
|
||||
|
||||
[CliCommand("copy_asset", "Copy an asset to a new path under the authoring root. The copy gets a fresh GUID.")]
|
||||
public static AuthoringResult CopyAsset(
|
||||
[CliArg("asset", "Reference to the asset to copy (path / guid / globalId).", Required = true)] ObjectRef asset,
|
||||
[CliArg("destination", "Destination asset path relative to the authoring root, including extension. The Assets/ prefix is optional.", Required = true)] string destination,
|
||||
[CliArg("confirm", "Required (true) only when overwriting an existing asset at the destination path.")] bool confirm = false,
|
||||
[CliArg("dry_run", "If true, validate inputs and report what would be copied without writing anything.")] bool dryRun = false)
|
||||
{
|
||||
var sourcePath = ResolveAssetPath(asset);
|
||||
|
||||
var dest = ProjectPaths.Resolve(destination, out var error);
|
||||
if (dest == null)
|
||||
throw new ArgumentException(error);
|
||||
|
||||
var exists = AssetDatabase.LoadMainAssetAtPath(dest) != null;
|
||||
if (exists && !confirm)
|
||||
throw new ArgumentException($"An asset already exists at '{dest}'. Pass confirm=true to overwrite it.");
|
||||
|
||||
if (dryRun)
|
||||
return new AuthoringResult { AssetPath = dest, Type = AssetDatabase.GetMainAssetTypeAtPath(sourcePath)?.Name };
|
||||
|
||||
EnsureParentFolder(dest);
|
||||
|
||||
// AssetDatabase.CopyAsset fails if the destination already exists, so delete it first when
|
||||
// the caller has confirmed an overwrite (the confirm guard above gates that).
|
||||
if (exists)
|
||||
AssetDatabase.DeleteAsset(dest);
|
||||
|
||||
if (!AssetDatabase.CopyAsset(sourcePath, dest))
|
||||
throw new ArgumentException($"Failed to copy '{sourcePath}' to '{dest}'.");
|
||||
|
||||
var loaded = AssetDatabase.LoadMainAssetAtPath(dest);
|
||||
var result = ObjectResolver.Describe(loaded) ?? new AuthoringResult();
|
||||
result.AssetPath = dest;
|
||||
return result;
|
||||
}
|
||||
|
||||
[CliCommand("rename_asset", "Rename an asset in place (keeps it in the same folder, keeps its GUID).")]
|
||||
public static AuthoringResult RenameAsset(
|
||||
[CliArg("asset", "Reference to the asset to rename (path / guid / globalId).", Required = true)] ObjectRef asset,
|
||||
[CliArg("new_name", "New file name WITHOUT a folder path. The extension is preserved if omitted.", Required = true)] string newName,
|
||||
[CliArg("dry_run", "If true, validate the rename without performing it.")] bool dryRun = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
throw new ArgumentException("new_name is required.");
|
||||
if (newName.Contains("/") || newName.Contains("\\"))
|
||||
throw new ArgumentException("new_name must be a file name only, not a path. Use move_asset to relocate an asset.");
|
||||
|
||||
var sourcePath = ResolveAssetPath(asset);
|
||||
|
||||
// Preserve the original extension when the agent omits one (AssetDatabase.RenameAsset expects no extension).
|
||||
var bareName = Path.GetFileNameWithoutExtension(newName);
|
||||
var extension = Path.GetExtension(sourcePath);
|
||||
var folder = Path.GetDirectoryName(sourcePath)?.Replace('\\', '/');
|
||||
var destPath = $"{folder}/{bareName}{extension}";
|
||||
|
||||
if (AssetDatabase.LoadMainAssetAtPath(destPath) != null)
|
||||
throw new ArgumentException($"An asset already exists at '{destPath}'.");
|
||||
|
||||
if (dryRun)
|
||||
return new AuthoringResult { AssetPath = destPath, Type = AssetDatabase.GetMainAssetTypeAtPath(sourcePath)?.Name };
|
||||
|
||||
var renameError = AssetDatabase.RenameAsset(sourcePath, bareName);
|
||||
if (!string.IsNullOrEmpty(renameError))
|
||||
throw new ArgumentException($"Failed to rename '{sourcePath}': {renameError}");
|
||||
|
||||
var loaded = AssetDatabase.LoadMainAssetAtPath(destPath);
|
||||
var result = ObjectResolver.Describe(loaded) ?? new AuthoringResult();
|
||||
result.AssetPath = destPath;
|
||||
return result;
|
||||
}
|
||||
|
||||
[CliCommand("delete_asset", "Delete an asset from the project. Destructive: requires confirm=true.")]
|
||||
public static AuthoringResult DeleteAsset(
|
||||
[CliArg("asset", "Reference to the asset to delete (path / guid / globalId).", Required = true)] ObjectRef asset,
|
||||
[CliArg("confirm", "Must be true to actually delete. Without it the command refuses (destructive guard).")] bool confirm = false,
|
||||
[CliArg("dry_run", "If true, report the asset that would be deleted without deleting it.")] bool dryRun = false)
|
||||
{
|
||||
var sourcePath = ResolveAssetPath(asset);
|
||||
|
||||
// Capture the identity before deletion so the agent gets a record of what was removed.
|
||||
var loaded = AssetDatabase.LoadMainAssetAtPath(sourcePath);
|
||||
var result = ObjectResolver.Describe(loaded) ?? new AuthoringResult { Type = AssetDatabase.GetMainAssetTypeAtPath(sourcePath)?.Name };
|
||||
result.AssetPath = sourcePath;
|
||||
|
||||
if (dryRun)
|
||||
return result;
|
||||
|
||||
if (!confirm)
|
||||
throw new ArgumentException($"Refusing to delete '{sourcePath}'. Pass confirm=true to delete it (destructive, not undoable via Unity's Undo).");
|
||||
|
||||
if (!AssetDatabase.DeleteAsset(sourcePath))
|
||||
throw new ArgumentException($"Failed to delete '{sourcePath}'.");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[CliCommand("find_assets", "Find assets by type and/or name and/or label, returning their path, GUID and type. At least one filter is required.")]
|
||||
public static FindAssetsResult FindAssets(
|
||||
[CliArg("type", "Type name to filter by (e.g. Material, GameObject, ScriptableObject, MyGame.GameConfig). Resolved to a System.Type and matched against each asset's actual main type.")] string type = null,
|
||||
[CliArg("name", "Name substring to filter by (AssetDatabase name filter).")] string name = null,
|
||||
[CliArg("label", "Asset label to filter by (AssetDatabase 'l:' filter).")] string label = null,
|
||||
[CliArg("search_in", "Folder to scope the search to, relative to the authoring root (default: the authoring root).")] string searchIn = null,
|
||||
[CliArg("limit", "Maximum number of results to return (default 200).")] int limit = 200)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(type) && string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(label))
|
||||
throw new ArgumentException("At least one of type, name, or label is required.");
|
||||
|
||||
// The type filter is applied by post-filtering on each asset's actual loaded type rather
|
||||
// than via AssetDatabase's "t:" token: "t:" does NOT reliably match freshly-created/custom
|
||||
// ScriptableObject assets (it can return 0 even for "t:ScriptableObject"), though it works
|
||||
// for built-in types. Name/label searches do work, so build the query from those only.
|
||||
var filterParts = new System.Collections.Generic.List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
filterParts.Add(name.Trim());
|
||||
if (!string.IsNullOrWhiteSpace(label))
|
||||
filterParts.Add($"l:{label.Trim()}");
|
||||
var filter = string.Join(" ", filterParts);
|
||||
|
||||
// Default the search scope to the authoring root (not the whole project) so an agent only
|
||||
// ever discovers assets within its sandbox unless an explicit scope is given.
|
||||
var scopeError = (string)null;
|
||||
var scope = !string.IsNullOrWhiteSpace(searchIn)
|
||||
? ProjectPaths.Resolve(searchIn, out scopeError)
|
||||
: ProjectPaths.AuthoringRoot;
|
||||
if (scope == null)
|
||||
throw new ArgumentException(scopeError);
|
||||
var searchFolders = new[] { scope };
|
||||
|
||||
// Resolve the type filter up front so an unresolvable name fails fast with a clear message.
|
||||
Type typeFilter = null;
|
||||
if (!string.IsNullOrWhiteSpace(type))
|
||||
{
|
||||
typeFilter = ResolveTypeName(type.Trim());
|
||||
if (typeFilter == null)
|
||||
throw new ArgumentException(
|
||||
$"Could not resolve type '{type}'. Use a short name (e.g. Material) or a fully-qualified name (e.g. MyGame.GameConfig).");
|
||||
}
|
||||
|
||||
// An empty name/label filter enumerates every asset under the search folders.
|
||||
var guids = AssetDatabase.FindAssets(filter ?? string.Empty, searchFolders);
|
||||
|
||||
var result = new FindAssetsResult { Filter = filter };
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (string.IsNullOrEmpty(assetPath))
|
||||
continue;
|
||||
|
||||
var mainType = AssetDatabase.GetMainAssetTypeAtPath(assetPath);
|
||||
if (typeFilter != null && (mainType == null || !typeFilter.IsAssignableFrom(mainType)))
|
||||
continue;
|
||||
|
||||
if (result.Assets.Count >= limit)
|
||||
{
|
||||
result.Truncated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
result.Assets.Add(new AuthoringResult
|
||||
{
|
||||
AssetPath = assetPath,
|
||||
Guid = guid,
|
||||
Type = mainType?.Name
|
||||
});
|
||||
}
|
||||
|
||||
result.Count = result.Assets.Count;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve a user-supplied type name (short or fully-qualified) to a <see cref="Type"/> by
|
||||
/// scanning non-dynamic loaded assemblies. Returns null when no match is found.
|
||||
/// </summary>
|
||||
private static Type ResolveTypeName(string typeName)
|
||||
{
|
||||
var direct = Type.GetType(typeName, throwOnError: false);
|
||||
if (direct != null)
|
||||
return direct;
|
||||
|
||||
var physicsMaterialType = ResolvePhysicsMaterialTypeName(typeName);
|
||||
if (physicsMaterialType != null)
|
||||
return physicsMaterialType;
|
||||
|
||||
foreach (var assembly in PipelineUtils.GetLoadedAssemblies())
|
||||
{
|
||||
if (assembly.IsDynamic)
|
||||
continue;
|
||||
|
||||
var byFullName = assembly.GetType(typeName, throwOnError: false);
|
||||
if (byFullName != null)
|
||||
return byFullName;
|
||||
}
|
||||
|
||||
foreach (var assembly in PipelineUtils.GetLoadedAssemblies())
|
||||
{
|
||||
if (assembly.IsDynamic)
|
||||
continue;
|
||||
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types;
|
||||
}
|
||||
|
||||
foreach (var candidate in types)
|
||||
{
|
||||
if (candidate != null && candidate.Name == typeName)
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve an <see cref="ObjectRef"/> to a project-relative asset path, rejecting handles that
|
||||
/// do not resolve to an on-disk asset (scene objects have no asset path). The resolved path is
|
||||
/// confined to the authoring root so a GUID/globalId handle cannot make a destructive command
|
||||
/// (delete/move/copy/rename) operate on an asset outside the sandbox.
|
||||
/// </summary>
|
||||
private static string ResolveAssetPath(ObjectRef asset)
|
||||
{
|
||||
var assetPath = ObjectResolver.TryResolve(asset, out var obj, out var error)
|
||||
? AssetDatabase.GetAssetPath(obj)
|
||||
: null;
|
||||
|
||||
// Fallback: a path reference that points at an asset which exists on disk (a GUID is
|
||||
// registered) but whose object cannot be loaded — e.g. a just-moved asset whose object
|
||||
// isn't reloadable this frame, or an asset with an unresolved/broken script. We can still
|
||||
// operate on it by path, so honor the path rather than failing the whole command.
|
||||
if (string.IsNullOrEmpty(assetPath) && !string.IsNullOrEmpty(asset?.Path))
|
||||
{
|
||||
var candidate = ProjectPaths.Resolve(asset.Path, out _);
|
||||
if (candidate != null && !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(candidate)))
|
||||
assetPath = candidate;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(assetPath))
|
||||
throw new ArgumentException(error ?? $"Reference '{asset}' does not point at an on-disk asset.");
|
||||
|
||||
// Confine to the authoring root. ProjectPaths.Resolve treats explicit "Assets/..." paths as
|
||||
// project-relative and rejects anything outside the configured root.
|
||||
var confined = ProjectPaths.Resolve(assetPath, out var confineError);
|
||||
if (confined == null)
|
||||
throw new ArgumentException(
|
||||
$"Asset '{assetPath}' is outside the authoring root '{ProjectPaths.AuthoringRoot}': {confineError}");
|
||||
|
||||
return confined;
|
||||
}
|
||||
|
||||
/// <summary>Create the asset's parent folder chain if it does not yet exist.</summary>
|
||||
private static void EnsureParentFolder(string assetPath)
|
||||
{
|
||||
var parent = Path.GetDirectoryName(assetPath)?.Replace('\\', '/');
|
||||
if (string.IsNullOrEmpty(parent) || AssetDatabase.IsValidFolder(parent))
|
||||
return;
|
||||
|
||||
CreateFolderRecursive(parent);
|
||||
}
|
||||
|
||||
private static void CreateFolderRecursive(string assetsPath)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(assetsPath))
|
||||
return;
|
||||
|
||||
var parent = Path.GetDirectoryName(assetsPath)?.Replace('\\', '/');
|
||||
var name = Path.GetFileName(assetsPath);
|
||||
if (string.IsNullOrEmpty(parent) || string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException($"Invalid folder path '{assetsPath}'.");
|
||||
|
||||
if (!AssetDatabase.IsValidFolder(parent))
|
||||
CreateFolderRecursive(parent);
|
||||
|
||||
AssetDatabase.CreateFolder(parent, name);
|
||||
}
|
||||
|
||||
/// <summary>Convert a project-relative asset path to an absolute filesystem path.</summary>
|
||||
private static string ToAbsolute(string projectRelative)
|
||||
{
|
||||
return $"{ProjectPaths.ProjectRoot.Replace('\\', '/')}/{projectRelative}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CLI-221: resolve the shader to assign to a newly-created <see cref="Material"/>.
|
||||
/// - An explicit <paramref name="shaderName"/> must resolve via <see cref="Shader.Find"/>;
|
||||
/// a miss is a clear, actionable error (no null Material / null-ref downstream).
|
||||
/// - When omitted, default to the active render pipeline's lit shader: "Universal Render
|
||||
/// Pipeline/Lit" when an SRP asset is active, else built-in "Standard". If that default
|
||||
/// can't be found, fall back to "Standard"; if even that is missing, error clearly.
|
||||
/// </summary>
|
||||
private static Shader ResolveShader(string shaderName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(shaderName))
|
||||
{
|
||||
var requested = Shader.Find(shaderName.Trim());
|
||||
if (requested == null)
|
||||
throw new ArgumentException($"Shader '{shaderName}' not found.");
|
||||
return requested;
|
||||
}
|
||||
|
||||
var usingSrp = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline != null;
|
||||
var defaultName = usingSrp ? "Universal Render Pipeline/Lit" : "Standard";
|
||||
|
||||
var defaultShader = Shader.Find(defaultName);
|
||||
if (defaultShader != null)
|
||||
return defaultShader;
|
||||
|
||||
// Fall back to the built-in Standard shader (always present in the Built-in RP, and a sane
|
||||
// last resort even under an SRP project that lacks the URP/Lit shader).
|
||||
var standard = Shader.Find("Standard");
|
||||
if (standard != null)
|
||||
return standard;
|
||||
|
||||
throw new ArgumentException(
|
||||
$"Could not resolve a default shader ('{defaultName}' or 'Standard'). Pass an explicit shader= argument.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve a user-supplied type name to a <see cref="Type"/>. Tries the name as-is (covers
|
||||
/// fully-qualified names), then scans loaded assemblies for an exact full-name or short-name
|
||||
/// match (covers "Material" or a user type given without its namespace).
|
||||
/// </summary>
|
||||
private static Type ResolveType(string typeName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(typeName))
|
||||
return null;
|
||||
|
||||
// Trim once up front so every resolution path below (special-case + assembly/type scans)
|
||||
// sees the same normalized name — leading/trailing whitespace must not be accepted for one
|
||||
// type and rejected for another.
|
||||
typeName = typeName.Trim();
|
||||
|
||||
// CLI-222: in Unity 6 the type is UnityEngine.PhysicsMaterial (renamed from PhysicMaterial).
|
||||
// A short name "PhysicsMaterial" resolves fine, but the legacy short name "PhysicMaterial"
|
||||
// matches the obsolete forwarder/alias inconsistently across versions — normalize BOTH the
|
||||
// current and legacy names (short or fully-qualified) to the real type up front so callers
|
||||
// can use either spelling.
|
||||
if (typeName == "PhysicsMaterial" || typeName == "PhysicMaterial"
|
||||
|| typeName == "UnityEngine.PhysicsMaterial" || typeName == "UnityEngine.PhysicMaterial")
|
||||
{
|
||||
return typeof(PipelinePhysicsMaterial);
|
||||
}
|
||||
|
||||
var direct = Type.GetType(typeName, throwOnError: false);
|
||||
if (direct != null)
|
||||
return direct;
|
||||
|
||||
foreach (var assembly in PipelineUtils.GetLoadedAssemblies())
|
||||
{
|
||||
var byFullName = assembly.GetType(typeName, throwOnError: false);
|
||||
if (byFullName != null)
|
||||
return byFullName;
|
||||
}
|
||||
|
||||
// Fall back to a short-name match across all loaded types.
|
||||
foreach (var assembly in PipelineUtils.GetLoadedAssemblies())
|
||||
{
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types;
|
||||
}
|
||||
|
||||
foreach (var candidate in types)
|
||||
{
|
||||
if (candidate != null && candidate.Name == typeName)
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Type ResolvePhysicsMaterialTypeName(string typeName)
|
||||
{
|
||||
if (typeName == "PhysicsMaterial" || typeName == "PhysicMaterial"
|
||||
|| typeName == "UnityEngine.PhysicsMaterial" || typeName == "UnityEngine.PhysicMaterial")
|
||||
{
|
||||
return typeof(PipelinePhysicsMaterial);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user