Add Unity Pipeline package support

This commit is contained in:
ud18010
2026-07-31 17:34:04 +08:00
parent d1a526094e
commit 786b7ffff9
590 changed files with 51995 additions and 2 deletions
@@ -0,0 +1,424 @@
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEditor.AI;
using UnityEngine;
// CLI-215 deliberately targets the built-in legacy UnityEditor.AI.NavMeshBuilder (see class doc). It is
// [Obsolete] in Unity 6000.x (the modern path is the com.unity.ai.navigation package), but is the
// intentional v1 baker here, so suppress CS0618 file-wide rather than migrate.
#pragma warning disable CS0618
namespace Unity.Pipeline.Editor.Commands.Baking
{
/// <summary>
/// CLI-215 Group B — NavMesh bake commands, targeting the <b>built-in legacy</b> baker
/// <see cref="UnityEditor.AI.NavMeshBuilder"/> (always available; bakes a single scene NavMesh from
/// the Navigation-static geometry of the open scene). Trigger an async build, poll its status,
/// cancel it, clear the baked NavMesh, and read/configure the default agent's bake settings.
///
/// The newer AI Navigation package (<c>com.unity.ai.navigation</c>, <c>NavMeshSurface</c>
/// components) is OUT for v1 — we do NOT add an asmdef reference to it. <c>bake_navmesh_surfaces</c>
/// is a guarded stub that probes for the package by reflection and returns
/// <c>{ code: "package_not_found" }</c> when it is absent (full support is a follow-up).
///
/// Async pattern mirrors <see cref="BuildCommand"/> / <see cref="LightingBakeCommands"/>: trigger
/// returns immediately, completion is detected by <see cref="NavMeshBuilder.isRunning"/> flipping
/// false (polled in <see cref="EditorApplication.update"/>), and in-flight state is held in static
/// fields reconciled against a Temp/ status file across domain reloads.
/// </summary>
[InitializeOnLoad]
public static class NavMeshBakeCommands
{
const string StatusFile = "Temp/pipeline_navmesh_bake_status.json";
// The serialized NavMesh bake-settings live on the Navigation window's settings object, keyed by
// these property names (stable across 2019+ / Unity 6).
const string PropAgentRadius = "m_BuildSettings.agentRadius";
const string PropAgentHeight = "m_BuildSettings.agentHeight";
const string PropAgentSlope = "m_BuildSettings.agentSlope";
const string PropAgentClimb = "m_BuildSettings.agentClimb";
const string PropMinRegionArea = "m_BuildSettings.minRegionArea";
const string PropManualCellSize = "m_BuildSettings.manualCellSize";
const string PropCellSize = "m_BuildSettings.cellSize";
static readonly object s_Lock = new object();
static bool s_Tracking;
static string s_BakeId;
static DateTime s_StartedUtc;
static NavMeshBakeCommands()
{
OnReload();
EditorApplication.update += OnUpdate;
}
#region bake_navmesh
[CliCommand("bake_navmesh", "Trigger an async legacy NavMesh bake of the open scene(s) via UnityEditor.AI.NavMeshBuilder. Returns immediately; poll navmesh_bake_status until completed.")]
public static object BakeNavMesh(
[CliArg("confirm", "Accepted for parity (a bake overwrites the existing NavMesh); not required.")] bool confirm = false,
[CliArg("dry_run", "If true, validate there is an open scene and return current NavMesh settings without baking.")] bool dryRun = false)
{
if (!BakeSceneGuard.HasBakeableScene())
return BakeSceneGuard.NoSceneResult();
OnUpdate(); // reconcile a possibly-stale tracking flag first.
if (NavMeshBuilder.isRunning)
return new { code = "bake_in_progress", message = "A NavMesh bake is already running. Poll navmesh_bake_status." };
if (dryRun)
return new { status = "dry_run", wouldBake = true, settings = ReadNavMeshSettings() };
var bakeId = Guid.NewGuid().ToString("N");
lock (s_Lock)
{
s_Tracking = true;
s_BakeId = bakeId;
s_StartedUtc = DateTime.UtcNow;
}
WriteStatus(new NavMeshBakeStatus
{
Status = "baking",
BakeId = bakeId,
StartedUtcTicks = s_StartedUtc.Ticks
});
NavMeshBuilder.BuildNavMeshAsync();
return new { status = "baking", bakeId };
}
[CliCommand("navmesh_bake_status", "Get the status of the last NavMesh bake: idle | baking | completed.", MainThreadRequired = false)]
public static string NavMeshBakeStatus()
{
if (File.Exists(StatusFile))
return File.ReadAllText(StatusFile);
return "{\"status\":\"idle\"}";
}
[CliCommand("cancel_navmesh_bake", "Cancel an in-progress NavMesh bake (NavMeshBuilder.Cancel()).")]
public static object CancelNavMeshBake()
{
var wasRunning = NavMeshBuilder.isRunning;
if (wasRunning)
NavMeshBuilder.Cancel();
lock (s_Lock)
{
if (s_Tracking)
{
var ms = (int)Math.Max(0, (DateTime.UtcNow - s_StartedUtc).TotalMilliseconds);
WriteStatus(new NavMeshBakeStatus
{
Status = "completed",
Result = "cancelled",
BakeId = s_BakeId,
BakeTimeMs = ms
});
s_Tracking = false;
}
}
return new { cancelled = wasRunning };
}
[CliCommand("clear_navmesh", "Clear the baked NavMesh for the open scene(s). Destructive: requires confirm=true.")]
public static object ClearNavMesh(
[CliArg("confirm", "Must be true to actually clear (destructive, not undoable via Unity's Undo).")] bool confirm = false,
[CliArg("dry_run", "If true, report what would be cleared without clearing.")] bool dryRun = false)
{
if (!confirm)
throw new ArgumentException("Refusing to clear the NavMesh. Pass confirm=true (destructive, not undoable via Unity's Undo).");
if (dryRun)
return new { status = "dry_run", wouldClear = true };
NavMeshBuilder.ClearAllNavMeshes();
return new { cleared = true };
}
#endregion
#region settings
[CliCommand("get_navmesh_settings", "Read the default agent's legacy NavMesh bake settings (agentRadius/Height/Slope/Climb, minRegionArea, voxelSize).")]
public static NavMeshSettingsResult GetNavMeshSettings()
{
return ReadNavMeshSettings();
}
[CliCommand("set_navmesh_settings", "Apply a subset of legacy NavMesh bake settings to the default agent. Returns { applied[], unknown[] }.")]
public static object SetNavMeshSettings(
[CliArg("settings", "JSON object with a subset of NavMesh fields to set (same names as get_navmesh_settings).", Required = true)] JObject settings,
[CliArg("dry_run", "If true, validate the keys and report applied/unknown without changing anything.")] bool dryRun = false)
{
if (settings == null)
throw new ArgumentException("settings is required (a JSON object of NavMesh fields to set).");
// NavMeshBuilder.navMeshSettingsObject is a UnityEngine.Object (the Navigation window's
// settings holder); wrap it in a SerializedObject to read/write the m_BuildSettings fields.
var settingsObject = NavMeshBuilder.navMeshSettingsObject;
if (settingsObject == null)
return new { code = "settings_unavailable", message = "Could not access the legacy NavMesh settings object." };
var so = new SerializedObject(settingsObject);
so.Update();
var applied = new List<string>();
var unknown = new List<string>();
foreach (var prop in settings.Properties())
{
if (TryApply(so, prop.Name, prop.Value, dryRun))
applied.Add(prop.Name);
else
unknown.Add(prop.Name);
}
if (!dryRun && applied.Count > 0)
so.ApplyModifiedPropertiesWithoutUndo();
return new { applied = applied.ToArray(), unknown = unknown.ToArray(), dryRun };
}
/// <summary>
/// Guarded stub for the AI Navigation package (<c>com.unity.ai.navigation</c>) NavMeshSurface
/// bake path. We never reference that assembly, so probe for the type by reflection; absent → the
/// documented <c>package_not_found</c> result. Full multi-surface baking is a follow-up.
/// </summary>
[CliCommand("bake_navmesh_surfaces", "Bake NavMeshSurface components (AI Navigation package). v1 stub: returns package_not_found when the package is absent.")]
public static object BakeNavMeshSurfaces()
{
var surfaceType = Type.GetType("Unity.AI.Navigation.NavMeshSurface, Unity.AI.Navigation")
?? Type.GetType("UnityEngine.AI.NavMeshSurface, Unity.AI.Navigation");
if (surfaceType == null)
return new
{
code = "package_not_found",
message = "The AI Navigation package (com.unity.ai.navigation) is not installed. " +
"NavMeshSurface-based baking is out of scope for v1; use bake_navmesh for the built-in baker."
};
return new
{
code = "not_implemented",
message = "AI Navigation package detected, but NavMeshSurface baking is not implemented in v1 (follow-up). Use bake_navmesh."
};
}
#endregion
#region completion / reload bookkeeping
static void OnUpdate()
{
lock (s_Lock)
{
if (s_Tracking && !NavMeshBuilder.isRunning)
Finish("succeeded");
}
}
// Must be called under s_Lock.
static void Finish(string result)
{
var ms = (int)Math.Max(0, (DateTime.UtcNow - s_StartedUtc).TotalMilliseconds);
WriteStatus(new NavMeshBakeStatus
{
Status = "completed",
Result = result,
BakeId = s_BakeId,
BakeTimeMs = ms
});
s_Tracking = false;
}
static void OnReload()
{
if (!File.Exists(StatusFile))
return;
NavMeshBakeStatus prior;
try
{
prior = JsonConvert.DeserializeObject<NavMeshBakeStatus>(File.ReadAllText(StatusFile));
}
catch
{
return;
}
if (prior == null || prior.Status != "baking")
return;
lock (s_Lock)
{
s_BakeId = prior.BakeId;
s_StartedUtc = prior.StartedUtcTicks > 0 ? new DateTime(prior.StartedUtcTicks, DateTimeKind.Utc) : DateTime.UtcNow;
s_Tracking = true;
if (!NavMeshBuilder.isRunning)
Finish("succeeded");
}
}
#endregion
#region settings read/write helpers
static NavMeshSettingsResult ReadNavMeshSettings()
{
var settingsObject = NavMeshBuilder.navMeshSettingsObject;
if (settingsObject == null)
return new NavMeshSettingsResult { Available = false };
var so = new SerializedObject(settingsObject);
so.Update();
var manual = FindBool(so, PropManualCellSize, false);
return new NavMeshSettingsResult
{
Available = true,
AgentRadius = FindFloat(so, PropAgentRadius, 0.5f),
AgentHeight = FindFloat(so, PropAgentHeight, 2f),
AgentSlope = FindFloat(so, PropAgentSlope, 45f),
AgentClimb = FindFloat(so, PropAgentClimb, 0.4f),
MinRegionArea = FindFloat(so, PropMinRegionArea, 2f),
VoxelSize = FindFloat(so, PropCellSize, 0f),
ManualVoxelSize = manual
};
}
static bool TryApply(SerializedObject so, string key, JToken value, bool dryRun)
{
switch (key)
{
case "agentRadius":
return SetFloat(so, PropAgentRadius, value, dryRun);
case "agentHeight":
return SetFloat(so, PropAgentHeight, value, dryRun);
case "agentSlope":
return SetFloat(so, PropAgentSlope, value, dryRun);
case "agentClimb":
return SetFloat(so, PropAgentClimb, value, dryRun);
case "minRegionArea":
return SetFloat(so, PropMinRegionArea, value, dryRun);
case "voxelSize":
// Setting an explicit voxel size implies manual override on.
{
var ok = SetFloat(so, PropCellSize, value, dryRun);
if (ok && !dryRun)
{
var manual = so.FindProperty(PropManualCellSize);
if (manual != null) manual.boolValue = true;
}
return ok;
}
case "manualVoxelSize":
return SetBool(so, PropManualCellSize, value, dryRun);
default:
return false;
}
}
static bool SetFloat(SerializedObject so, string propPath, JToken value, bool dryRun)
{
var prop = so.FindProperty(propPath);
if (prop == null)
return false;
var f = value.Value<float>();
if (!dryRun) prop.floatValue = f;
return true;
}
static bool SetBool(SerializedObject so, string propPath, JToken value, bool dryRun)
{
var prop = so.FindProperty(propPath);
if (prop == null)
return false;
var b = value.Value<bool>();
if (!dryRun) prop.boolValue = b;
return true;
}
static float FindFloat(SerializedObject so, string propPath, float fallback)
{
var prop = so.FindProperty(propPath);
return prop != null ? prop.floatValue : fallback;
}
static bool FindBool(SerializedObject so, string propPath, bool fallback)
{
var prop = so.FindProperty(propPath);
return prop != null ? prop.boolValue : fallback;
}
#endregion
static void WriteStatus(NavMeshBakeStatus status)
{
try
{
File.WriteAllText(StatusFile, JsonConvert.SerializeObject(status));
}
catch (Exception ex)
{
Debug.LogError($"[NavMeshBake] Failed to write status file: {ex.Message}");
}
}
}
/// <summary>Status/result payload for <c>bake_navmesh</c> / <c>navmesh_bake_status</c>.</summary>
[Serializable]
public class NavMeshBakeStatus
{
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("bakeId")]
public string BakeId { get; set; }
[JsonProperty("result", NullValueHandling = NullValueHandling.Ignore)]
public string Result { get; set; }
[JsonProperty("bakeTimeMs")]
public int BakeTimeMs { get; set; }
[JsonProperty("startedUtcTicks")]
public long StartedUtcTicks { get; set; }
}
/// <summary>Result of <c>get_navmesh_settings</c> (default-agent legacy bake settings).</summary>
[Serializable]
public class NavMeshSettingsResult
{
[JsonProperty("available")]
public bool Available { get; set; }
[JsonProperty("agentRadius")]
public float AgentRadius { get; set; }
[JsonProperty("agentHeight")]
public float AgentHeight { get; set; }
[JsonProperty("agentSlope")]
public float AgentSlope { get; set; }
[JsonProperty("agentClimb")]
public float AgentClimb { get; set; }
[JsonProperty("minRegionArea")]
public float MinRegionArea { get; set; }
[JsonProperty("voxelSize")]
public float VoxelSize { get; set; }
[JsonProperty("manualVoxelSize")]
public bool ManualVoxelSize { get; set; }
}
}