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,116 @@
using System.Collections.Generic;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEngine;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Get/set the project Audio settings (CLI-202). The AudioManager has no scripting API, so this
/// reads/writes its serialized properties (global volume, rolloff scale, doppler factor) via
/// <see cref="ProjectSettingsAsset"/>. Properties absent in a given Unity version are skipped.
/// </summary>
public static class AudioSettingsCommands
{
const string Group = "audio";
const string AssetPath = "ProjectSettings/AudioManager.asset";
// Serialized property name -> response/plan label.
const string VolumeProp = "m_Volume";
const string RolloffProp = "Rolloff Scale";
const string DopplerProp = "Doppler Factor";
[CliCommand("get_audio_settings", "Read project Audio settings (volume, rolloff scale, doppler factor).", MainThreadRequired = true)]
public static ProjectSettingsResponse Get() => ProjectSettingsCommand.Get(Group, Read);
[CliCommand("set_audio_settings", "Change project Audio settings. Requires confirm=true; use dry_run to preview. Not undoable via Ctrl+Z.", MainThreadRequired = true)]
public static ProjectSettingsResponse Set(
[CliArg("settings", "Fields to change; omitted fields are left unchanged.")] AudioSettingsInput settings = null,
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false)
{
if (settings == null)
return ProjectSettingsResponse.Fail(Group, "No 'settings' object provided.");
var so = ProjectSettingsAsset.Load(AssetPath);
var changes = new List<string>();
AddFloatChange(so, VolumeProp, "volume", settings.Volume, changes);
AddFloatChange(so, RolloffProp, "rolloffScale", settings.RolloffScale, changes);
AddFloatChange(so, DopplerProp, "dopplerFactor", settings.DopplerFactor, changes);
if (changes.Count == 0)
return NoChanges();
var planText = "Set audio settings: " + string.Join("; ", changes);
void Apply()
{
var obj = ProjectSettingsAsset.Load(AssetPath);
SetFloat(obj, VolumeProp, settings.Volume);
SetFloat(obj, RolloffProp, settings.RolloffScale);
SetFloat(obj, DopplerProp, settings.DopplerFactor);
obj.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
}
return ProjectSettingsCommand.Apply(Group, confirm, dryRun, settings, () => planText, Apply, Read);
}
static ProjectSettingsResponse NoChanges()
{
var response = ProjectSettingsCommand.Get(Group, Read);
response.Message = "No changes specified; nothing to apply.";
return response;
}
static Dictionary<string, object> Read()
{
var so = ProjectSettingsAsset.Load(AssetPath);
var values = new Dictionary<string, object>();
AddFloat(so, VolumeProp, "volume", values);
AddFloat(so, RolloffProp, "rolloffScale", values);
AddFloat(so, DopplerProp, "dopplerFactor", values);
return values;
}
static void AddFloat(SerializedObject so, string prop, string label, Dictionary<string, object> values)
{
var p = so.FindProperty(prop);
if (p != null)
values[label] = p.floatValue;
}
static void AddFloatChange(SerializedObject so, string prop, string label, float? newValue, List<string> changes)
{
if (!newValue.HasValue)
return;
var p = so.FindProperty(prop);
if (p == null)
return; // not present in this Unity version
if (!Mathf.Approximately(p.floatValue, newValue.Value))
changes.Add($"{label} {p.floatValue} -> {newValue.Value}");
}
static void SetFloat(SerializedObject so, string prop, float? value)
{
if (!value.HasValue)
return;
var p = so.FindProperty(prop);
if (p != null)
p.floatValue = value.Value;
}
}
/// <summary>Audio settings to change. Null/omitted fields are left unchanged.</summary>
public class AudioSettingsInput : IStructuredCommandInput
{
[CliArg("volume", "Global audio volume (0..1).")]
public float? Volume { get; set; }
[CliArg("rolloffScale", "Global rolloff scale.")]
public float? RolloffScale { get; set; }
[CliArg("dopplerFactor", "Global doppler factor.")]
public float? DopplerFactor { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a7abd0c1722ba34dae96408746d2c9a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,87 @@
using System.Collections.Generic;
using Unity.Pipeline.Commands;
using Unity.Pipeline.Editor.Authoring;
using Unity.Pipeline.Models;
using UnityEditor;
using UnityEngine.Rendering;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Get/set the project's default render pipeline (CLI-202). The settable field is a handle to a
/// <see cref="RenderPipelineAsset"/>; an empty reference selects the built-in pipeline (no SRP).
/// </summary>
public static class GraphicsSettingsCommands
{
const string Group = "graphics";
[CliCommand("get_graphics_settings", "Read GraphicsSettings (default render pipeline).", MainThreadRequired = true)]
public static ProjectSettingsResponse Get() => ProjectSettingsCommand.Get(Group, Read);
[CliCommand("set_graphics_settings", "Set the default render pipeline asset. Requires confirm=true; use dry_run to preview. Not undoable via Ctrl+Z.", MainThreadRequired = true)]
public static ProjectSettingsResponse Set(
[CliArg("settings", "Fields to change; omitted fields are left unchanged.")] GraphicsSettingsInput settings = null,
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false)
{
if (settings == null)
return ProjectSettingsResponse.Fail(Group, "No 'settings' object provided.");
// omitted (null) = leave unchanged; an empty reference = built-in pipeline; otherwise a
// handle to a RenderPipelineAsset resolved through the shared ObjectResolver.
if (settings.RenderPipelineAsset == null)
return NoChanges();
RenderPipelineAsset asset = null;
if (!settings.RenderPipelineAsset.IsEmpty)
{
if (!ObjectResolver.TryResolve(settings.RenderPipelineAsset, out var obj, out var resolveError))
return ProjectSettingsResponse.Fail(Group, resolveError);
asset = obj as RenderPipelineAsset;
if (asset == null)
return ProjectSettingsResponse.Fail(Group, $"'{settings.RenderPipelineAsset}' is not a RenderPipelineAsset.");
}
var current = GraphicsSettings.defaultRenderPipeline;
if (current == asset)
return NoChanges();
var currentName = current != null ? current.name : "<built-in>";
var newName = asset != null ? asset.name : "<built-in>";
var planText = $"Set defaultRenderPipeline {currentName} -> {newName}";
void Apply()
{
GraphicsSettings.defaultRenderPipeline = asset;
AssetDatabase.SaveAssets();
}
return ProjectSettingsCommand.Apply(Group, confirm, dryRun, settings, () => planText, Apply, Read);
}
static ProjectSettingsResponse NoChanges()
{
var response = ProjectSettingsCommand.Get(Group, Read);
response.Message = "No changes specified; nothing to apply.";
return response;
}
static Dictionary<string, object> Read()
{
var srp = GraphicsSettings.defaultRenderPipeline;
return new Dictionary<string, object>
{
["defaultRenderPipeline"] = srp != null ? srp.name : null,
["defaultRenderPipelinePath"] = srp != null ? AssetDatabase.GetAssetPath(srp) : null,
["usingScriptableRenderPipeline"] = srp != null
};
}
}
/// <summary>Graphics settings to change. Null/omitted fields are left unchanged.</summary>
public class GraphicsSettingsInput : IStructuredCommandInput
{
[CliArg("renderPipelineAsset", "Reference (path / guid / globalId) to a RenderPipelineAsset to set as the default. Pass an empty reference ({}) to select the built-in pipeline; omit to leave unchanged.")]
public ObjectRef RenderPipelineAsset { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5095c024b3a03e94996b73e4e42c4e4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,150 @@
using System.Collections.Generic;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEngine;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Get/set the legacy Input Manager (CLI-202). The InputManager has no scripting API, so this
/// reads/writes <c>ProjectSettings/InputManager.asset</c> via serialized properties:
/// <c>get</c> lists the configured axis names; <c>set</c> tunes a named axis's
/// sensitivity/gravity/dead values.
///
/// "Input (legacy / Input System where feasible)" per the ticket: the legacy InputManager is
/// always present, so it is the portable target. Projects on the new Input System package store
/// their config in a separate input-actions asset, which would be a follow-up.
/// </summary>
public static class InputSettingsCommands
{
const string Group = "input";
const string AssetPath = "ProjectSettings/InputManager.asset";
[CliCommand("get_input_settings", "Read the legacy Input Manager axes (names and count).", MainThreadRequired = true)]
public static ProjectSettingsResponse Get() => ProjectSettingsCommand.Get(Group, Read);
[CliCommand("set_input_settings", "Tune a legacy Input Manager axis (sensitivity/gravity/dead) by name. Requires confirm=true; use dry_run to preview. Not undoable via Ctrl+Z.", MainThreadRequired = true)]
public static ProjectSettingsResponse Set(
[CliArg("settings", "Axis change. 'axis' selects the axis by name; omitted numeric fields are left unchanged.")] InputAxisInput settings = null,
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false)
{
if (settings == null)
return ProjectSettingsResponse.Fail(Group, "No 'settings' object provided.");
if (string.IsNullOrEmpty(settings.Axis))
return ProjectSettingsResponse.Fail(Group, "'axis' is required.");
var so = ProjectSettingsAsset.Load(AssetPath);
var axes = so.FindProperty("m_Axes");
var index = FindAxisIndex(axes, settings.Axis);
if (index < 0)
return ProjectSettingsResponse.Fail(Group, $"No axis named '{settings.Axis}'.");
var axis = axes.GetArrayElementAtIndex(index);
var changes = new List<string>();
AddFloatChange(axis, "sensitivity", settings.Sensitivity, changes);
AddFloatChange(axis, "gravity", settings.Gravity, changes);
AddFloatChange(axis, "dead", settings.Dead, changes);
if (changes.Count == 0)
return NoChanges();
var planText = $"Set input axis '{settings.Axis}': " + string.Join("; ", changes);
void Apply()
{
var obj = ProjectSettingsAsset.Load(AssetPath);
var arr = obj.FindProperty("m_Axes");
var idx = FindAxisIndex(arr, settings.Axis);
if (idx < 0)
return;
var ax = arr.GetArrayElementAtIndex(idx);
SetFloat(ax, "sensitivity", settings.Sensitivity);
SetFloat(ax, "gravity", settings.Gravity);
SetFloat(ax, "dead", settings.Dead);
obj.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
}
return ProjectSettingsCommand.Apply(Group, confirm, dryRun, settings, () => planText, Apply, Read);
}
static ProjectSettingsResponse NoChanges()
{
var response = ProjectSettingsCommand.Get(Group, Read);
response.Message = "No changes specified; nothing to apply.";
return response;
}
static int FindAxisIndex(SerializedProperty axes, string name)
{
if (axes == null)
return -1;
for (var i = 0; i < axes.arraySize; i++)
{
var nameProp = axes.GetArrayElementAtIndex(i).FindPropertyRelative("m_Name");
if (nameProp != null && nameProp.stringValue == name)
return i;
}
return -1;
}
static void AddFloatChange(SerializedProperty axis, string prop, float? newValue, List<string> changes)
{
if (!newValue.HasValue)
return;
var p = axis.FindPropertyRelative(prop);
if (p == null)
return;
if (!Mathf.Approximately(p.floatValue, newValue.Value))
changes.Add($"{prop} {p.floatValue} -> {newValue.Value}");
}
static void SetFloat(SerializedProperty axis, string prop, float? value)
{
if (!value.HasValue)
return;
var p = axis.FindPropertyRelative(prop);
if (p != null)
p.floatValue = value.Value;
}
static Dictionary<string, object> Read()
{
var so = ProjectSettingsAsset.Load(AssetPath);
var axes = so.FindProperty("m_Axes");
var names = new List<string>();
if (axes != null)
{
for (var i = 0; i < axes.arraySize; i++)
{
var nameProp = axes.GetArrayElementAtIndex(i).FindPropertyRelative("m_Name");
names.Add(nameProp != null ? nameProp.stringValue : null);
}
}
return new Dictionary<string, object>
{
["axisCount"] = names.Count,
["axes"] = names
};
}
}
/// <summary>Selects a legacy input axis by name and the numeric fields to change (omitted = unchanged).</summary>
public class InputAxisInput : IStructuredCommandInput
{
[CliArg("axis", "Name of the axis to modify (e.g. 'Horizontal').", Required = true)]
public string Axis { get; set; }
[CliArg("sensitivity", "New sensitivity.")]
public float? Sensitivity { get; set; }
[CliArg("gravity", "New gravity.")]
public float? Gravity { get; set; }
[CliArg("dead", "New dead-zone size.")]
public float? Dead { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 27a9903daaf869948aa6dd45e9735f6f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,98 @@
using System.Collections.Generic;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEngine;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Get/set a representative slice of 3D <see cref="Physics"/> settings (CLI-202): gravity (by
/// component), default solver iterations, and the bounce threshold.
/// </summary>
public static class PhysicsSettingsCommands
{
const string Group = "physics";
[CliCommand("get_physics_settings", "Read Physics settings (gravity, solver iterations, bounce threshold).", MainThreadRequired = true)]
public static ProjectSettingsResponse Get() => ProjectSettingsCommand.Get(Group, Read);
[CliCommand("set_physics_settings", "Change Physics settings. Requires confirm=true; use dry_run to preview. Not undoable via Ctrl+Z.", MainThreadRequired = true)]
public static ProjectSettingsResponse Set(
[CliArg("settings", "Fields to change; omitted fields are left unchanged.")] PhysicsSettingsInput settings = null,
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false)
{
if (settings == null)
return ProjectSettingsResponse.Fail(Group, "No 'settings' object provided.");
var changes = new List<string>();
var gravity = Physics.gravity;
var newGravity = gravity;
if (settings.GravityX.HasValue) newGravity.x = settings.GravityX.Value;
if (settings.GravityY.HasValue) newGravity.y = settings.GravityY.Value;
if (settings.GravityZ.HasValue) newGravity.z = settings.GravityZ.Value;
if (newGravity != gravity)
changes.Add($"gravity {gravity} -> {newGravity}");
if (settings.DefaultSolverIterations.HasValue && settings.DefaultSolverIterations.Value != Physics.defaultSolverIterations)
changes.Add($"defaultSolverIterations {Physics.defaultSolverIterations} -> {settings.DefaultSolverIterations.Value}");
if (settings.BounceThreshold.HasValue && !Mathf.Approximately(settings.BounceThreshold.Value, Physics.bounceThreshold))
changes.Add($"bounceThreshold {Physics.bounceThreshold} -> {settings.BounceThreshold.Value}");
if (changes.Count == 0)
return NoChanges();
var planText = "Set physics settings: " + string.Join("; ", changes);
void Apply()
{
Physics.gravity = newGravity;
if (settings.DefaultSolverIterations.HasValue) Physics.defaultSolverIterations = settings.DefaultSolverIterations.Value;
if (settings.BounceThreshold.HasValue) Physics.bounceThreshold = settings.BounceThreshold.Value;
AssetDatabase.SaveAssets();
}
return ProjectSettingsCommand.Apply(Group, confirm, dryRun, settings, () => planText, Apply, Read);
}
static ProjectSettingsResponse NoChanges()
{
var response = ProjectSettingsCommand.Get(Group, Read);
response.Message = "No changes specified; nothing to apply.";
return response;
}
static Dictionary<string, object> Read()
{
var g = Physics.gravity;
return new Dictionary<string, object>
{
["gravityX"] = g.x,
["gravityY"] = g.y,
["gravityZ"] = g.z,
["defaultSolverIterations"] = Physics.defaultSolverIterations,
["bounceThreshold"] = Physics.bounceThreshold
};
}
}
/// <summary>Physics settings to change. Null/omitted fields are left unchanged.</summary>
public class PhysicsSettingsInput : IStructuredCommandInput
{
[CliArg("gravityX", "Gravity X component.")]
public float? GravityX { get; set; }
[CliArg("gravityY", "Gravity Y component (e.g. -9.81).")]
public float? GravityY { get; set; }
[CliArg("gravityZ", "Gravity Z component.")]
public float? GravityZ { get; set; }
[CliArg("defaultSolverIterations", "Default solver iteration count.")]
public int? DefaultSolverIterations { get; set; }
[CliArg("bounceThreshold", "Bounce threshold velocity.")]
public float? BounceThreshold { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 06de808693ce8c24891baa1026490d3d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,122 @@
using System.Collections.Generic;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEditor.Build;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Get/set a representative slice of <see cref="PlayerSettings"/> (CLI-202): identity
/// (company/product/version) and the two domain-reload-triggering toggles — scripting backend and
/// API compatibility level. Scripting-backend / API-level changes are read and written against the
/// active build target's <see cref="NamedBuildTarget"/>.
///
/// Icons and splash are intentionally out of this MVP set: they take <c>Texture2D</c> assets, not
/// scalar JSON, so they need a dedicated asset-reference convention.
/// </summary>
public static class PlayerSettingsCommands
{
const string Group = "player";
[CliCommand("get_player_settings", "Read PlayerSettings (company/product/version, scripting backend, API level).", MainThreadRequired = true)]
public static ProjectSettingsResponse Get() => ProjectSettingsCommand.Get(Group, Read);
[CliCommand("set_player_settings", "Change PlayerSettings. Requires confirm=true; use dry_run to preview. Not undoable via Ctrl+Z. Scripting backend / API level changes trigger a domain reload.", MainThreadRequired = true)]
public static ProjectSettingsResponse Set(
[CliArg("settings", "Fields to change; omitted fields are left unchanged.")] PlayerSettingsInput settings = null,
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false)
{
if (settings == null)
return ProjectSettingsResponse.Fail(Group, "No 'settings' object provided.");
var target = NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
var changes = new List<string>();
var reload = false;
if (settings.CompanyName != null && settings.CompanyName != PlayerSettings.companyName)
changes.Add($"companyName '{PlayerSettings.companyName}' -> '{settings.CompanyName}'");
if (settings.ProductName != null && settings.ProductName != PlayerSettings.productName)
changes.Add($"productName '{PlayerSettings.productName}' -> '{settings.ProductName}'");
if (settings.BundleVersion != null && settings.BundleVersion != PlayerSettings.bundleVersion)
changes.Add($"bundleVersion '{PlayerSettings.bundleVersion}' -> '{settings.BundleVersion}'");
if (settings.ScriptingBackend.HasValue)
{
var current = PlayerSettings.GetScriptingBackend(target);
if (settings.ScriptingBackend.Value != current)
{
changes.Add($"scriptingBackend {current} -> {settings.ScriptingBackend.Value} (domain reload)");
reload = true;
}
}
if (settings.ApiCompatibilityLevel.HasValue)
{
var current = PlayerSettings.GetApiCompatibilityLevel(target);
if (settings.ApiCompatibilityLevel.Value != current)
{
changes.Add($"apiCompatibilityLevel {current} -> {settings.ApiCompatibilityLevel.Value} (domain reload)");
reload = true;
}
}
if (changes.Count == 0)
return NoChanges();
var planText = "Set player settings: " + string.Join("; ", changes);
void Apply()
{
if (settings.CompanyName != null) PlayerSettings.companyName = settings.CompanyName;
if (settings.ProductName != null) PlayerSettings.productName = settings.ProductName;
if (settings.BundleVersion != null) PlayerSettings.bundleVersion = settings.BundleVersion;
if (settings.ScriptingBackend.HasValue) PlayerSettings.SetScriptingBackend(target, settings.ScriptingBackend.Value);
if (settings.ApiCompatibilityLevel.HasValue) PlayerSettings.SetApiCompatibilityLevel(target, settings.ApiCompatibilityLevel.Value);
AssetDatabase.SaveAssets();
}
return ProjectSettingsCommand.Apply(Group, confirm, dryRun, settings, () => planText, Apply, Read, requiresDomainReload: reload);
}
static ProjectSettingsResponse NoChanges()
{
var response = ProjectSettingsCommand.Get(Group, Read);
response.Message = "No changes specified; nothing to apply.";
return response;
}
static Dictionary<string, object> Read()
{
var target = NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
return new Dictionary<string, object>
{
["companyName"] = PlayerSettings.companyName,
["productName"] = PlayerSettings.productName,
["bundleVersion"] = PlayerSettings.bundleVersion,
["buildTarget"] = target.TargetName,
["scriptingBackend"] = PlayerSettings.GetScriptingBackend(target).ToString(),
["apiCompatibilityLevel"] = PlayerSettings.GetApiCompatibilityLevel(target).ToString()
};
}
}
/// <summary>Player settings to change. Null/omitted fields are left unchanged.</summary>
public class PlayerSettingsInput : IStructuredCommandInput
{
[CliArg("companyName", "Company name.")]
public string CompanyName { get; set; }
[CliArg("productName", "Product name.")]
public string ProductName { get; set; }
[CliArg("bundleVersion", "Bundle/application version string.")]
public string BundleVersion { get; set; }
[CliArg("scriptingBackend", "Scripting backend (e.g. Mono2x, IL2CPP). Triggers a domain reload.")]
public ScriptingImplementation? ScriptingBackend { get; set; }
[CliArg("apiCompatibilityLevel", "API compatibility level (e.g. NET_Standard_2_0, NET_Unity_4_8). Triggers a domain reload.")]
public ApiCompatibilityLevel? ApiCompatibilityLevel { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4fcf409834ac0bf4bb18a7b7fadad8dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
using System;
using UnityEditor;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Loads a <see cref="SerializedObject"/> for a singleton settings asset under
/// <c>ProjectSettings/</c> (e.g. <c>InputManager.asset</c>, <c>TagManager.asset</c>,
/// <c>AudioManager.asset</c>) that has no first-class scripting API. Used by the settings-group
/// commands that must read/write those managers via serialized properties.
/// </summary>
internal static class ProjectSettingsAsset
{
public static SerializedObject Load(string assetPath)
{
var objects = AssetDatabase.LoadAllAssetsAtPath(assetPath);
if (objects == null || objects.Length == 0 || objects[0] == null)
throw new InvalidOperationException($"Could not load settings asset at '{assetPath}'.");
return new SerializedObject(objects[0]);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5646d5f43f10f344ab42eecab65f08c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Shared plumbing the per-group project-settings commands (CLI-202) wrap, so each group's
/// <c>get</c>/<c>set</c> handler stays thin and the policy is applied uniformly:
/// <list type="bullet">
/// <item><description><see cref="Get"/> — read a group's values into a <see cref="ProjectSettingsResponse"/>.</description></item>
/// <item><description><see cref="Apply"/> — gate a write with the <c>confirm</c>/<c>dry_run</c> convention and
/// apply it (project-settings writes are not part of Unity's Undo), then re-read the group so the response
/// reflects the post-change state, and surface domain-reload / reimport signals.</description></item>
/// </list>
///
/// Group handlers supply two callbacks: <c>readValues</c> (snapshot the group's current values)
/// and, for writes, <c>apply</c> (perform the mutation) plus a <c>plan</c> describing the intended
/// effect. All run on the main thread (these commands are <c>MainThreadRequired = true</c>).
/// </summary>
public static class ProjectSettingsCommand
{
/// <summary>
/// Read-only path: build a success response carrying the group's current values.
/// </summary>
public static ProjectSettingsResponse Get(string group, Func<Dictionary<string, object>> readValues)
{
if (readValues == null)
throw new ArgumentNullException(nameof(readValues));
if (!TryReadValues(readValues, out var values, out var readError))
return ProjectSettingsResponse.Fail(group, $"Failed to read {group} settings: {readError}");
return new ProjectSettingsResponse
{
Success = true,
Group = group,
Values = values,
Message = $"Read {group} settings."
};
}
/// <summary>
/// Write path: gate the change with the <c>confirm</c>/<c>dry_run</c> convention and apply it
/// (project-settings writes are not undoable via Ctrl+Z), then re-read the
/// group. <paramref name="requiresDomainReload"/> / <paramref name="requiresReimport"/> declare
/// side effects of this specific change (Unity defers them, so the response returns first);
/// they are reported for an applied change and for a dry-run preview alike, and suppressed when
/// the operation is refused or fails.
/// </summary>
public static ProjectSettingsResponse Apply(
string group,
bool confirm,
bool dryRun,
object auditParams,
Func<string> plan,
Action apply,
Func<Dictionary<string, object>> readValues,
bool requiresDomainReload = false,
bool requiresReimport = false)
{
if (plan == null)
throw new ArgumentNullException(nameof(plan));
if (apply == null)
throw new ArgumentNullException(nameof(apply));
if (readValues == null)
throw new ArgumentNullException(nameof(readValues));
string planText;
try
{
planText = plan();
}
catch (Exception ex)
{
return ProjectSettingsResponse.Fail(group, $"Failed to compute the operation preview: {ex.Message}");
}
// Inline confirm / dry_run gate + Undo grouping (the codebase-wide mutating convention).
var response = new ProjectSettingsResponse { Group = group };
if (dryRun)
{
response.Success = true;
response.DryRun = true;
response.Applied = false;
response.Message = $"Dry run — no changes made. Intended effect: {planText}";
}
else if (!confirm)
{
response.Success = false;
response.Applied = false;
response.Message =
"Refused: this operation mutates the project. Re-run with confirm=true to apply, " +
"or dry_run=true to preview the change.";
}
else
{
try
{
apply();
response.Success = true;
response.Applied = true;
response.Message = planText;
}
catch (Exception ex)
{
response.Success = false;
response.Applied = false;
response.Message = $"Operation failed: {ex.Message}";
}
}
// Side-effect signals are only meaningful when the operation went through (applied) or was
// previewed (dry run); suppress them for a refusal or failure.
response.RequiresDomainReload = response.Success && requiresDomainReload;
response.RequiresReimport = response.Success && requiresReimport;
// Re-read so the caller sees the resulting (or, for a dry run / refusal, the unchanged)
// state. A read failure must not mask the write outcome, so it is non-fatal here.
if (TryReadValues(readValues, out var values, out var readError))
response.Values = values;
else
Debug.LogWarning($"[pipeline] Read-back of {group} settings failed: {readError}");
return response;
}
private static bool TryReadValues(Func<Dictionary<string, object>> readValues,
out Dictionary<string, object> values, out string error)
{
try
{
values = readValues() ?? new Dictionary<string, object>();
error = null;
return true;
}
catch (Exception ex)
{
values = null;
error = ex.Message;
return false;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 00e8d8eb6857c9d40babc72bb711b9c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// <para>
/// Shared response for the project-settings get/set commands (CLI-202). One type backs every
/// settings group (PlayerSettings, Quality, Graphics, Physics, Time, Input, Tags &amp; Layers,
/// Audio) so agents get a uniform shape across them.
/// </para>
///
/// <para><see cref="Values"/> carries the current settings for the group as a flat
/// name → value map — returned by a <c>get</c>, and re-read after a <c>set</c> so the caller can
/// confirm the change without a second round-trip.</para>
///
/// <para>Writes follow the shared <c>confirm</c>/<c>dry_run</c> convention, so
/// <see cref="Applied"/> / <see cref="DryRun"/> mirror its semantics: a change only mutates when
/// it actually ran (not a preview, not refused). <see cref="RequiresDomainReload"/> and
/// <see cref="RequiresReimport"/> flag side effects an agent must wait out — e.g. switching the
/// scripting backend or API level triggers a domain reload; some graphics/texture changes trigger
/// an asset reimport. They describe the operation, so a <c>dry_run</c> still reports them.</para>
/// </summary>
[Serializable]
public class ProjectSettingsResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
/// <summary>Settings group this response is for (e.g. "player", "quality", "time").</summary>
[JsonProperty("group")]
public string Group { get; set; }
/// <summary>True only when a write actually mutated the project (false for get/dry-run/refused/failed).</summary>
[JsonProperty("applied")]
public bool Applied { get; set; }
/// <summary>True when a write was a preview only (no mutation).</summary>
[JsonProperty("dryRun")]
public bool DryRun { get; set; }
/// <summary>The operation triggers a domain reload; the agent should wait for the editor to settle.</summary>
[JsonProperty("requiresDomainReload")]
public bool RequiresDomainReload { get; set; }
/// <summary>The operation triggers an asset reimport; the agent should wait for it to finish.</summary>
[JsonProperty("requiresReimport")]
public bool RequiresReimport { get; set; }
/// <summary>Current settings for the group as a name → value map.</summary>
[JsonProperty("values")]
public Dictionary<string, object> Values { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
public static ProjectSettingsResponse Fail(string group, string message) => new ProjectSettingsResponse
{
Success = false,
Group = group,
Message = message
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f8c1356c54a05248b34aff8e0490c0c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,95 @@
using System.Collections.Generic;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEngine;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Get/set <see cref="QualitySettings"/> (CLI-202): the active quality level plus a couple of
/// representative per-level toggles (vSync, anti-aliasing). Level changes apply expensive changes
/// so the switch takes full effect.
/// </summary>
public static class QualitySettingsCommands
{
const string Group = "quality";
[CliCommand("get_quality_settings", "Read QualitySettings (current level, level names, vSync, anti-aliasing).", MainThreadRequired = true)]
public static ProjectSettingsResponse Get() => ProjectSettingsCommand.Get(Group, Read);
[CliCommand("set_quality_settings", "Change QualitySettings. Requires confirm=true; use dry_run to preview. Not undoable via Ctrl+Z.", MainThreadRequired = true)]
public static ProjectSettingsResponse Set(
[CliArg("settings", "Fields to change; omitted fields are left unchanged.")] QualitySettingsInput settings = null,
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false)
{
if (settings == null)
return ProjectSettingsResponse.Fail(Group, "No 'settings' object provided.");
var names = QualitySettings.names;
var changes = new List<string>();
if (settings.Level.HasValue)
{
if (settings.Level.Value < 0 || settings.Level.Value >= names.Length)
return ProjectSettingsResponse.Fail(Group, $"level {settings.Level.Value} out of range (0..{names.Length - 1}).");
if (settings.Level.Value != QualitySettings.GetQualityLevel())
changes.Add($"level {QualitySettings.GetQualityLevel()} -> {settings.Level.Value}");
}
if (settings.VSyncCount.HasValue && settings.VSyncCount.Value != QualitySettings.vSyncCount)
changes.Add($"vSyncCount {QualitySettings.vSyncCount} -> {settings.VSyncCount.Value}");
if (settings.AntiAliasing.HasValue && settings.AntiAliasing.Value != QualitySettings.antiAliasing)
changes.Add($"antiAliasing {QualitySettings.antiAliasing} -> {settings.AntiAliasing.Value}");
if (changes.Count == 0)
return NoChanges();
var planText = "Set quality settings: " + string.Join("; ", changes);
void Apply()
{
if (settings.Level.HasValue) QualitySettings.SetQualityLevel(settings.Level.Value, true);
if (settings.VSyncCount.HasValue) QualitySettings.vSyncCount = settings.VSyncCount.Value;
if (settings.AntiAliasing.HasValue) QualitySettings.antiAliasing = settings.AntiAliasing.Value;
AssetDatabase.SaveAssets();
}
return ProjectSettingsCommand.Apply(Group, confirm, dryRun, settings, () => planText, Apply, Read);
}
static ProjectSettingsResponse NoChanges()
{
var response = ProjectSettingsCommand.Get(Group, Read);
response.Message = "No changes specified; nothing to apply.";
return response;
}
static Dictionary<string, object> Read()
{
var names = QualitySettings.names;
var level = QualitySettings.GetQualityLevel();
return new Dictionary<string, object>
{
["level"] = level,
["levelName"] = (level >= 0 && level < names.Length) ? names[level] : null,
["levelNames"] = names,
["vSyncCount"] = QualitySettings.vSyncCount,
["antiAliasing"] = QualitySettings.antiAliasing
};
}
}
/// <summary>Quality settings to change. Null/omitted fields are left unchanged.</summary>
public class QualitySettingsInput : IStructuredCommandInput
{
[CliArg("level", "Quality level index (see levelNames from get_quality_settings).")]
public int? Level { get; set; }
[CliArg("vSyncCount", "VSync count (0 = off, 1, 2).")]
public int? VSyncCount { get; set; }
[CliArg("antiAliasing", "MSAA sample count (0, 2, 4, 8).")]
public int? AntiAliasing { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b4bc1a976dcac514796a70fd1b91a2be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,143 @@
using System.Collections.Generic;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEditorInternal;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Get/set project tags and layers (CLI-202). Tags use the
/// <see cref="InternalEditorUtility"/> add/remove API; user layers (indices 8..31; 0..7 are
/// reserved) are assigned via the TagManager's serialized <c>layers</c> array.
/// </summary>
public static class TagsLayersCommands
{
const string Group = "tags_layers";
const string TagManagerPath = "ProjectSettings/TagManager.asset";
const int FirstUserLayer = 8;
const int LastUserLayer = 31;
[CliCommand("get_tags_layers", "Read the project's tags and (named) layers.", MainThreadRequired = true)]
public static ProjectSettingsResponse Get() => ProjectSettingsCommand.Get(Group, Read);
[CliCommand("set_tags_layers", "Add/remove tags and assign user layer names (index 8-31). Requires confirm=true; use dry_run to preview. Not undoable via Ctrl+Z.", MainThreadRequired = true)]
public static ProjectSettingsResponse Set(
[CliArg("settings", "Tag/layer changes to make.")] TagsLayersInput settings = null,
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false)
{
if (settings == null)
return ProjectSettingsResponse.Fail(Group, "No 'settings' object provided.");
if (settings.SetLayers != null)
{
foreach (var layer in settings.SetLayers)
{
if (layer == null)
continue;
if (layer.Index < FirstUserLayer || layer.Index > LastUserLayer)
return ProjectSettingsResponse.Fail(Group,
$"layer index {layer.Index} is reserved or out of range; user layers are {FirstUserLayer}..{LastUserLayer}.");
}
}
var existingTags = new HashSet<string>(InternalEditorUtility.tags);
var existingLayers = InternalEditorUtility.layers;
var changes = new List<string>();
if (settings.AddTags != null)
foreach (var tag in settings.AddTags)
if (!string.IsNullOrEmpty(tag) && !existingTags.Contains(tag))
changes.Add($"add tag '{tag}'");
if (settings.RemoveTags != null)
foreach (var tag in settings.RemoveTags)
if (!string.IsNullOrEmpty(tag) && existingTags.Contains(tag))
changes.Add($"remove tag '{tag}'");
if (settings.SetLayers != null)
foreach (var layer in settings.SetLayers)
if (layer != null)
{
var desired = layer.Name ?? string.Empty;
var current = (existingLayers != null && layer.Index >= 0 && layer.Index < existingLayers.Length)
? (existingLayers[layer.Index] ?? string.Empty)
: string.Empty;
if (current != desired)
changes.Add($"layer[{layer.Index}] = '{desired}'");
}
if (changes.Count == 0)
return NoChanges();
var planText = "Set tags/layers: " + string.Join("; ", changes);
void Apply()
{
// Remove before add so a remove+re-add in one call behaves predictably.
if (settings.RemoveTags != null)
foreach (var tag in settings.RemoveTags)
if (!string.IsNullOrEmpty(tag))
InternalEditorUtility.RemoveTag(tag);
if (settings.AddTags != null)
foreach (var tag in settings.AddTags)
if (!string.IsNullOrEmpty(tag))
InternalEditorUtility.AddTag(tag);
if (settings.SetLayers != null && settings.SetLayers.Length > 0)
{
var so = ProjectSettingsAsset.Load(TagManagerPath);
var layersProp = so.FindProperty("layers");
foreach (var layer in settings.SetLayers)
{
if (layer == null)
continue;
layersProp.GetArrayElementAtIndex(layer.Index).stringValue = layer.Name ?? string.Empty;
}
so.ApplyModifiedProperties();
}
AssetDatabase.SaveAssets();
}
return ProjectSettingsCommand.Apply(Group, confirm, dryRun, settings, () => planText, Apply, Read);
}
static ProjectSettingsResponse NoChanges()
{
var response = ProjectSettingsCommand.Get(Group, Read);
response.Message = "No changes specified; nothing to apply.";
return response;
}
static Dictionary<string, object> Read() => new Dictionary<string, object>
{
["tags"] = InternalEditorUtility.tags,
["layers"] = InternalEditorUtility.layers
};
}
/// <summary>Tag/layer changes. All fields optional; omitted ones make no change.</summary>
public class TagsLayersInput : IStructuredCommandInput
{
[CliArg("addTags", "Tag names to add.")]
public string[] AddTags { get; set; }
[CliArg("removeTags", "Tag names to remove.")]
public string[] RemoveTags { get; set; }
[CliArg("setLayers", "User layer assignments (index 8-31).")]
public LayerAssignment[] SetLayers { get; set; }
}
/// <summary>Assigns a name to a single user layer slot.</summary>
public class LayerAssignment : IStructuredCommandInput
{
[CliArg("index", "Layer index (8-31 for user layers).", Required = true)]
public int Index { get; set; }
[CliArg("name", "Layer name (empty string clears the slot).", Required = true)]
public string Name { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c6cb0f1d0a407f498fc141814f2d329
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,80 @@
using System.Collections.Generic;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEngine;
namespace Unity.Pipeline.Editor.Commands.ProjectSettings
{
/// <summary>
/// Get/set the project Time settings (CLI-202): fixed timestep, maximum allowed timestep, and the
/// time scale.
/// </summary>
public static class TimeSettingsCommands
{
const string Group = "time";
[CliCommand("get_time_settings", "Read Time settings (fixedDeltaTime, maximumDeltaTime, timeScale).", MainThreadRequired = true)]
public static ProjectSettingsResponse Get() => ProjectSettingsCommand.Get(Group, Read);
[CliCommand("set_time_settings", "Change Time settings. Requires confirm=true; use dry_run to preview. Not undoable via Ctrl+Z.", MainThreadRequired = true)]
public static ProjectSettingsResponse Set(
[CliArg("settings", "Fields to change; omitted fields are left unchanged.")] TimeSettingsInput settings = null,
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false)
{
if (settings == null)
return ProjectSettingsResponse.Fail(Group, "No 'settings' object provided.");
var changes = new List<string>();
if (settings.FixedDeltaTime.HasValue && !Mathf.Approximately(settings.FixedDeltaTime.Value, Time.fixedDeltaTime))
changes.Add($"fixedDeltaTime {Time.fixedDeltaTime} -> {settings.FixedDeltaTime.Value}");
if (settings.MaximumDeltaTime.HasValue && !Mathf.Approximately(settings.MaximumDeltaTime.Value, Time.maximumDeltaTime))
changes.Add($"maximumDeltaTime {Time.maximumDeltaTime} -> {settings.MaximumDeltaTime.Value}");
if (settings.TimeScale.HasValue && !Mathf.Approximately(settings.TimeScale.Value, Time.timeScale))
changes.Add($"timeScale {Time.timeScale} -> {settings.TimeScale.Value}");
if (changes.Count == 0)
return NoChanges();
var planText = "Set time settings: " + string.Join("; ", changes);
void Apply()
{
if (settings.FixedDeltaTime.HasValue) Time.fixedDeltaTime = settings.FixedDeltaTime.Value;
if (settings.MaximumDeltaTime.HasValue) Time.maximumDeltaTime = settings.MaximumDeltaTime.Value;
if (settings.TimeScale.HasValue) Time.timeScale = settings.TimeScale.Value;
AssetDatabase.SaveAssets();
}
return ProjectSettingsCommand.Apply(Group, confirm, dryRun, settings, () => planText, Apply, Read);
}
static ProjectSettingsResponse NoChanges()
{
var response = ProjectSettingsCommand.Get(Group, Read);
response.Message = "No changes specified; nothing to apply.";
return response;
}
static Dictionary<string, object> Read() => new Dictionary<string, object>
{
["fixedDeltaTime"] = Time.fixedDeltaTime,
["maximumDeltaTime"] = Time.maximumDeltaTime,
["timeScale"] = Time.timeScale
};
}
/// <summary>Time settings to change. Null/omitted fields are left unchanged.</summary>
public class TimeSettingsInput : IStructuredCommandInput
{
[CliArg("fixedDeltaTime", "Fixed timestep in seconds (e.g. 0.02).")]
public float? FixedDeltaTime { get; set; }
[CliArg("maximumDeltaTime", "Maximum allowed timestep in seconds.")]
public float? MaximumDeltaTime { get; set; }
[CliArg("timeScale", "Time scale (1 = real-time).")]
public float? TimeScale { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a5973463d405c064687fe56fee5d011a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: