using System;
#if UNITY_EDITOR
using System.IO;
#endif
using UnityEngine;
namespace Unity.Pipeline.Config
{
///
/// Configuration for Pipeline server functionality in Unity Player builds. Authored settings
/// live in a JSON file under ProjectSettings/ (see /) —
/// never as an asset under Assets/, so nothing about this package appears in the user's
/// Project window. At real Player build time, PipelineRuntimeBuildProcessor bakes the current
/// settings into a transient Resources asset (deleted again after the build) so the Player
/// can find them via Resources.Load.
///
public class RuntimePipelineConfig : ScriptableObject
{
/// Master switch. The server only starts when true. Off by default for safety.
[Tooltip("Enable Pipeline HTTP server in Player builds. SECURITY WARNING: Only enable in development/QA builds, never production without proper security measures.")]
public bool enableInBuilds = false;
/// Listen port. 0 auto-assigns from 7900-7999.
[Header("Server")]
[Tooltip("HTTP port for Pipeline server. Use 0 for auto-assignment from range 7900-7999.")]
public int port = 0;
/// Per-request timeout, in milliseconds.
[Tooltip("Request timeout in milliseconds. Higher values allow longer-running commands.")]
[Range(1000, 60000)]
public int requestTimeoutMs = 30000;
/// Log remote requests for auditing.
[Header("Runtime Behavior")]
[Tooltip("Enable detailed logging of all remote requests for security auditing.")]
public bool enableAuditLogging = true;
/// Start the server automatically when the Player boots (or Play Mode is entered).
[Tooltip("Start the server automatically when the Player boots (or Play Mode is entered).")]
public bool autoStart = true;
/// Dispatcher work items processed per frame.
[Tooltip("Maximum work items the dispatcher processes per frame to maintain performance.")]
[Range(1, 50)]
public int maxWorkItemsPerFrame = 10;
// Plain int field (not [Range]) so the Inspector shows a text field rather than an
// unusable 0-65535 slider; the bound is still enforced here, same pattern as
// EditorPipelineManager.OnValidate's m_WatchdogIntervalSeconds clamp.
private void OnValidate()
{
port = Mathf.Clamp(port, 0, 65535);
}
/// Resource name the build-time-generated transient asset is saved under for to find in a Player build.
public const string ResourceName = "RuntimePipelineConfig";
#if UNITY_EDITOR
/// Project-relative path of the authored settings file (never under Assets/).
private const string SettingsFilePath = "ProjectSettings/Packages/com.unity.pipeline/RuntimePipelineConfig.json";
#endif
///
/// Load the current settings, or null if none exist yet. In the Editor (including Play
/// Mode) this reads the authored ProjectSettings/ JSON file directly; in a real Player
/// build it reads the build-time-generated transient Resources asset.
///
/// The current settings, or null if none exist yet.
public static RuntimePipelineConfig Load()
{
#if UNITY_EDITOR
var path = GetAbsoluteSettingsPath();
if (!File.Exists(path))
return null;
var config = CreateInstance();
try
{
JsonUtility.FromJsonOverwrite(File.ReadAllText(path), config);
}
catch (Exception ex)
{
Debug.LogWarning($"Pipeline: could not read runtime settings at '{path}' ({ex.Message}); using no config. Delete the file to reset.");
DestroyImmediate(config);
return null;
}
return config;
#else
return Resources.Load(ResourceName);
#endif
}
#if UNITY_EDITOR
private static int s_LiveMaxWorkItemsPerFrameCache = 10;
private static DateTime s_LiveMaxWorkItemsPerFrameCacheFileTime;
///
/// Cheap, per-frame-callable read of just maxWorkItemsPerFrame from the authored settings
/// file, for RuntimePipelineDriver.Update() to poll without paying Load()'s full
/// CreateInstance+JSON-parse cost every frame. Re-parses only when the file's last-write
/// time changes, so a Project Settings edit (including while in Play Mode) takes effect on
/// the very next frame, while an idle frame costs only a single file-timestamp check.
/// Editor-only: a Player's config is frozen at build time, so there is nothing to poll for.
///
public static int GetLiveMaxWorkItemsPerFrame(int fallback)
{
var path = GetAbsoluteSettingsPath();
if (!File.Exists(path))
return fallback;
var writeTime = File.GetLastWriteTimeUtc(path);
if (writeTime != s_LiveMaxWorkItemsPerFrameCacheFileTime)
{
var config = Load();
if (config != null)
{
s_LiveMaxWorkItemsPerFrameCache = config.maxWorkItemsPerFrame;
DestroyImmediate(config);
}
s_LiveMaxWorkItemsPerFrameCacheFileTime = writeTime;
}
return s_LiveMaxWorkItemsPerFrameCache;
}
/// Persist this instance's current field values to the ProjectSettings/ JSON file.
public void Save()
{
var path = GetAbsoluteSettingsPath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
var previousWrite = File.Exists(path) ? File.GetLastWriteTimeUtc(path) : DateTime.MinValue;
File.WriteAllText(path, JsonUtility.ToJson(this, true));
// GetLiveMaxWorkItemsPerFrame and the settings page detect changes by last-write time.
// Windows stamps writes with the system clock tick (~15 ms), so two saves in quick
// succession can share a stamp and the second one goes unnoticed. Make every save
// observable by moving the stamp forward when the file system did not.
if (File.GetLastWriteTimeUtc(path) <= previousWrite)
File.SetLastWriteTimeUtc(path, previousWrite.AddMilliseconds(1));
}
///
/// Last-write-time of the authored settings file (UTC), or DateTime.MinValue if it doesn't
/// exist yet. Lets a long-lived caller (the Project Settings page) detect that the file
/// changed underneath it — e.g. a build's "Disable Pipeline" security-dialog choice, or a
/// set_runtime_pipeline_settings CLI call — without reloading from disk on every frame.
///
public static DateTime GetSettingsFileWriteTimeUtc()
{
var path = GetAbsoluteSettingsPath();
return File.Exists(path) ? File.GetLastWriteTimeUtc(path) : DateTime.MinValue;
}
/// Delete the authored settings file, if any. For test cleanup only.
public static void DeleteSettingsFileForTesting()
{
var path = GetAbsoluteSettingsPath();
if (File.Exists(path))
File.Delete(path);
}
private static string GetAbsoluteSettingsPath()
{
// Application.dataPath is ".../Assets"; ProjectSettings is a sibling directory.
var projectRoot = Path.GetDirectoryName(Application.dataPath);
return Path.Combine(projectRoot, SettingsFilePath);
}
#endif
///
/// Validate the configuration for correctness.
/// Called by build processor to ensure safe deployment.
///
/// The validation result.
public ValidationResult Validate()
{
if (!enableInBuilds)
return ValidationResult.Success("Runtime Pipeline disabled");
// Port validation
if (port != 0 && (port < 7900 || port > 7999))
{
return ValidationResult.Warning(
$"Port {port} is outside recommended runtime range 7900-7999. May conflict with Editor instances.");
}
return ValidationResult.Success("Configuration is valid");
}
}
///
/// Result from configuration validation.
///
[Serializable]
public class ValidationResult
{
/// False only for results.
public bool IsValid { get; set; }
/// "success", "warning", or "error".
public string Level { get; set; } // "success", "warning", "error"
/// Human-readable validation message.
public string Message { get; set; }
/// Create a successful validation result.
/// Human-readable message.
/// A valid, "success"-level result.
public static ValidationResult Success(string message = "Valid") =>
new ValidationResult { IsValid = true, Level = "success", Message = message };
/// Create a valid-but-noteworthy validation result.
/// Human-readable message.
/// A valid, "warning"-level result.
public static ValidationResult Warning(string message) =>
new ValidationResult { IsValid = true, Level = "warning", Message = message };
/// Create a failed validation result.
/// Human-readable message.
/// An invalid, "error"-level result.
public static ValidationResult Error(string message) =>
new ValidationResult { IsValid = false, Level = "error", Message = message };
}
}