diff --git a/MCPForUnity/Editor/Helpers/RendererHelpers.cs b/MCPForUnity/Editor/Helpers/RendererHelpers.cs
new file mode 100644
index 000000000..83eab85af
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/RendererHelpers.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+using UnityEngine;
+using UnityEditor;
+
+namespace MCPForUnity.Editor.Helpers
+{
+ ///
+ /// Utility class for common Renderer property operations.
+ /// Used by ManageVFX for ParticleSystem, LineRenderer, and TrailRenderer components.
+ ///
+ public static class RendererHelpers
+ {
+
+ ///
+ /// Applies common Renderer properties (shadows, lighting, probes, sorting, rendering layer).
+ /// Used by ParticleSetRenderer, LineSetProperties, TrailSetProperties.
+ ///
+ public static void ApplyCommonRendererProperties(Renderer renderer, JObject @params, List changes)
+ {
+ // Shadows
+ if (@params["shadowCastingMode"] != null && Enum.TryParse(@params["shadowCastingMode"].ToString(), true, out var shadowMode))
+ { renderer.shadowCastingMode = shadowMode; changes.Add("shadowCastingMode"); }
+ if (@params["receiveShadows"] != null) { renderer.receiveShadows = @params["receiveShadows"].ToObject(); changes.Add("receiveShadows"); }
+ // Note: shadowBias is only available on specific renderer types (e.g., ParticleSystemRenderer), not base Renderer
+
+ // Lighting and probes
+ if (@params["lightProbeUsage"] != null && Enum.TryParse(@params["lightProbeUsage"].ToString(), true, out var probeUsage))
+ { renderer.lightProbeUsage = probeUsage; changes.Add("lightProbeUsage"); }
+ if (@params["reflectionProbeUsage"] != null && Enum.TryParse(@params["reflectionProbeUsage"].ToString(), true, out var reflectionUsage))
+ { renderer.reflectionProbeUsage = reflectionUsage; changes.Add("reflectionProbeUsage"); }
+
+ // Motion vectors
+ if (@params["motionVectorGenerationMode"] != null && Enum.TryParse(@params["motionVectorGenerationMode"].ToString(), true, out var motionMode))
+ { renderer.motionVectorGenerationMode = motionMode; changes.Add("motionVectorGenerationMode"); }
+
+ // Sorting
+ if (@params["sortingOrder"] != null) { renderer.sortingOrder = @params["sortingOrder"].ToObject(); changes.Add("sortingOrder"); }
+ if (@params["sortingLayerName"] != null) { renderer.sortingLayerName = @params["sortingLayerName"].ToString(); changes.Add("sortingLayerName"); }
+ if (@params["sortingLayerID"] != null) { renderer.sortingLayerID = @params["sortingLayerID"].ToObject(); changes.Add("sortingLayerID"); }
+
+ // Rendering layer mask (for SRP)
+ if (@params["renderingLayerMask"] != null) { renderer.renderingLayerMask = @params["renderingLayerMask"].ToObject(); changes.Add("renderingLayerMask"); }
+ }
+
+ ///
+ /// Gets common Renderer properties for GetInfo methods.
+ ///
+ public static object GetCommonRendererInfo(Renderer renderer)
+ {
+ return new
+ {
+ shadowCastingMode = renderer.shadowCastingMode.ToString(),
+ receiveShadows = renderer.receiveShadows,
+ lightProbeUsage = renderer.lightProbeUsage.ToString(),
+ reflectionProbeUsage = renderer.reflectionProbeUsage.ToString(),
+ sortingOrder = renderer.sortingOrder,
+ sortingLayerName = renderer.sortingLayerName,
+ renderingLayerMask = renderer.renderingLayerMask
+ };
+ }
+
+
+ ///
+ /// Sets width properties for LineRenderer or TrailRenderer.
+ ///
+ /// JSON parameters containing width, startWidth, endWidth, widthCurve, widthMultiplier
+ /// List to track changed properties
+ /// Action to set start width
+ /// Action to set end width
+ /// Action to set width curve
+ /// Action to set width multiplier
+ /// Function to parse animation curve from JToken
+ public static void ApplyWidthProperties(JObject @params, List changes,
+ Action setStartWidth, Action setEndWidth,
+ Action setWidthCurve, Action setWidthMultiplier,
+ Func parseAnimationCurve)
+ {
+ if (@params["width"] != null)
+ {
+ float w = @params["width"].ToObject();
+ setStartWidth(w);
+ setEndWidth(w);
+ changes.Add("width");
+ }
+ if (@params["startWidth"] != null) { setStartWidth(@params["startWidth"].ToObject()); changes.Add("startWidth"); }
+ if (@params["endWidth"] != null) { setEndWidth(@params["endWidth"].ToObject()); changes.Add("endWidth"); }
+ if (@params["widthCurve"] != null) { setWidthCurve(parseAnimationCurve(@params["widthCurve"], 1f)); changes.Add("widthCurve"); }
+ if (@params["widthMultiplier"] != null) { setWidthMultiplier(@params["widthMultiplier"].ToObject()); changes.Add("widthMultiplier"); }
+ }
+
+ ///
+ /// Sets color properties for LineRenderer or TrailRenderer.
+ ///
+ /// JSON parameters containing color, startColor, endColor, gradient
+ /// List to track changed properties
+ /// Action to set start color
+ /// Action to set end color
+ /// Action to set gradient
+ /// Function to parse color from JToken
+ /// Function to parse gradient from JToken
+ /// If true, sets end color alpha to 0 when using single color
+ public static void ApplyColorProperties(JObject @params, List changes,
+ Action setStartColor, Action setEndColor,
+ Action setGradient,
+ Func parseColor, Func parseGradient,
+ bool fadeEndAlpha = false)
+ {
+ if (@params["color"] != null)
+ {
+ Color c = parseColor(@params["color"]);
+ setStartColor(c);
+ setEndColor(fadeEndAlpha ? new Color(c.r, c.g, c.b, 0f) : c);
+ changes.Add("color");
+ }
+ if (@params["startColor"] != null) { setStartColor(parseColor(@params["startColor"])); changes.Add("startColor"); }
+ if (@params["endColor"] != null) { setEndColor(parseColor(@params["endColor"])); changes.Add("endColor"); }
+ if (@params["gradient"] != null) { setGradient(parseGradient(@params["gradient"])); changes.Add("gradient"); }
+ }
+
+
+ ///
+ /// Sets material for a Renderer.
+ ///
+ /// The renderer to set material on
+ /// JSON parameters containing materialPath
+ /// Name for the undo operation
+ /// Function to find material by path
+ public static object SetRendererMaterial(Renderer renderer, JObject @params, string undoName, Func findMaterial)
+ {
+ if (renderer == null) return new { success = false, message = "Renderer not found" };
+
+ string path = @params["materialPath"]?.ToString();
+ if (string.IsNullOrEmpty(path)) return new { success = false, message = "materialPath required" };
+
+ Material mat = findMaterial(path);
+ if (mat == null) return new { success = false, message = $"Material not found: {path}" };
+
+ Undo.RecordObject(renderer, undoName);
+ renderer.sharedMaterial = mat;
+ EditorUtility.SetDirty(renderer);
+
+ return new { success = true, message = $"Set material to {mat.name}" };
+ }
+
+
+ ///
+ /// Applies Line/Trail specific properties (loop, alignment, textureMode, etc.).
+ ///
+ public static void ApplyLineTrailProperties(JObject @params, List changes,
+ Action setLoop, Action setUseWorldSpace,
+ Action setNumCornerVertices, Action setNumCapVertices,
+ Action setAlignment, Action setTextureMode,
+ Action setGenerateLightingData)
+ {
+ if (@params["loop"] != null && setLoop != null) { setLoop(@params["loop"].ToObject()); changes.Add("loop"); }
+ if (@params["useWorldSpace"] != null && setUseWorldSpace != null) { setUseWorldSpace(@params["useWorldSpace"].ToObject()); changes.Add("useWorldSpace"); }
+ if (@params["numCornerVertices"] != null && setNumCornerVertices != null) { setNumCornerVertices(@params["numCornerVertices"].ToObject()); changes.Add("numCornerVertices"); }
+ if (@params["numCapVertices"] != null && setNumCapVertices != null) { setNumCapVertices(@params["numCapVertices"].ToObject()); changes.Add("numCapVertices"); }
+ if (@params["alignment"] != null && setAlignment != null && Enum.TryParse(@params["alignment"].ToString(), true, out var align)) { setAlignment(align); changes.Add("alignment"); }
+ if (@params["textureMode"] != null && setTextureMode != null && Enum.TryParse(@params["textureMode"].ToString(), true, out var texMode)) { setTextureMode(texMode); changes.Add("textureMode"); }
+ if (@params["generateLightingData"] != null && setGenerateLightingData != null) { setGenerateLightingData(@params["generateLightingData"].ToObject()); changes.Add("generateLightingData"); }
+ }
+
+ }
+}
+
diff --git a/MCPForUnity/Editor/Helpers/RendererHelpers.cs.meta b/MCPForUnity/Editor/Helpers/RendererHelpers.cs.meta
new file mode 100644
index 000000000..db81f32a7
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/RendererHelpers.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 8f3a7e2d5c1b4a9e6d0f8c3b2a1e5d7c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+
diff --git a/MCPForUnity/Editor/Helpers/VectorParsing.cs b/MCPForUnity/Editor/Helpers/VectorParsing.cs
index a2e0a6cc6..a7337e005 100644
--- a/MCPForUnity/Editor/Helpers/VectorParsing.cs
+++ b/MCPForUnity/Editor/Helpers/VectorParsing.cs
@@ -1,13 +1,14 @@
using System;
+using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
///
- /// Utility class for parsing JSON tokens into Unity vector and math types.
+ /// Utility class for parsing JSON tokens into Unity vector, math, and animation types.
/// Supports both array format [x, y, z] and object format {x: 1, y: 2, z: 3}.
- ///
+ ///
public static class VectorParsing
{
///
@@ -224,6 +225,242 @@ public static Vector3 ParseVector3OrDefault(JToken token, Vector3 defaultValue =
return null;
}
+ ///
+ /// Parses a JToken into a Color, returning a default value if parsing fails.
+ /// Added for ManageVFX refactoring.
+ ///
+ public static Color ParseColorOrDefault(JToken token, Color defaultValue = default)
+ {
+ if (defaultValue == default) defaultValue = Color.black;
+ return ParseColor(token) ?? defaultValue;
+ }
+
+
+ ///
+ /// Parses a JToken (array or object) into a Vector4.
+ /// Added for ManageVFX refactoring.
+ ///
+ /// The JSON token to parse
+ /// The parsed Vector4 or null if parsing fails
+ public static Vector4? ParseVector4(JToken token)
+ {
+ if (token == null || token.Type == JTokenType.Null)
+ return null;
+
+ try
+ {
+ // Array format: [x, y, z, w]
+ if (token is JArray array && array.Count >= 4)
+ {
+ return new Vector4(
+ array[0].ToObject(),
+ array[1].ToObject(),
+ array[2].ToObject(),
+ array[3].ToObject()
+ );
+ }
+
+ // Object format: {x: 1, y: 2, z: 3, w: 4}
+ if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z") && obj.ContainsKey("w"))
+ {
+ return new Vector4(
+ obj["x"].ToObject(),
+ obj["y"].ToObject(),
+ obj["z"].ToObject(),
+ obj["w"].ToObject()
+ );
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"[VectorParsing] Failed to parse Vector4 from '{token}': {ex.Message}");
+ }
+
+ return null;
+ }
+
+ ///
+ /// Parses a JToken into a Vector4, returning a default value if parsing fails.
+ /// Added for ManageVFX refactoring.
+ ///
+ public static Vector4 ParseVector4OrDefault(JToken token, Vector4 defaultValue = default)
+ {
+ return ParseVector4(token) ?? defaultValue;
+ }
+
+ ///
+ /// Parses a JToken into a Gradient.
+ /// Supports formats:
+ /// - Simple: {startColor: [r,g,b,a], endColor: [r,g,b,a]}
+ /// - Full: {colorKeys: [{color: [r,g,b,a], time: 0.0}, ...], alphaKeys: [{alpha: 1.0, time: 0.0}, ...]}
+ /// Added for ManageVFX refactoring.
+ ///
+ /// The JSON token to parse
+ /// The parsed Gradient or null if parsing fails
+ public static Gradient ParseGradient(JToken token)
+ {
+ if (token == null || token.Type == JTokenType.Null)
+ return null;
+
+ try
+ {
+ Gradient gradient = new Gradient();
+
+ if (token is JObject obj)
+ {
+ // Simple format: {startColor: ..., endColor: ...}
+ if (obj.ContainsKey("startColor"))
+ {
+ Color startColor = ParseColorOrDefault(obj["startColor"]);
+ Color endColor = ParseColorOrDefault(obj["endColor"] ?? obj["startColor"]);
+ float startAlpha = obj["startAlpha"]?.ToObject() ?? startColor.a;
+ float endAlpha = obj["endAlpha"]?.ToObject() ?? endColor.a;
+
+ gradient.SetKeys(
+ new GradientColorKey[] { new GradientColorKey(startColor, 0f), new GradientColorKey(endColor, 1f) },
+ new GradientAlphaKey[] { new GradientAlphaKey(startAlpha, 0f), new GradientAlphaKey(endAlpha, 1f) }
+ );
+ return gradient;
+ }
+
+ // Full format: {colorKeys: [...], alphaKeys: [...]}
+ var colorKeys = new List();
+ var alphaKeys = new List();
+
+ if (obj["colorKeys"] is JArray colorKeysArr)
+ {
+ foreach (var key in colorKeysArr)
+ {
+ Color color = ParseColorOrDefault(key["color"]);
+ float time = key["time"]?.ToObject() ?? 0f;
+ colorKeys.Add(new GradientColorKey(color, time));
+ }
+ }
+
+ if (obj["alphaKeys"] is JArray alphaKeysArr)
+ {
+ foreach (var key in alphaKeysArr)
+ {
+ float alpha = key["alpha"]?.ToObject() ?? 1f;
+ float time = key["time"]?.ToObject() ?? 0f;
+ alphaKeys.Add(new GradientAlphaKey(alpha, time));
+ }
+ }
+
+ // Ensure at least 2 keys
+ if (colorKeys.Count == 0)
+ {
+ colorKeys.Add(new GradientColorKey(Color.white, 0f));
+ colorKeys.Add(new GradientColorKey(Color.white, 1f));
+ }
+
+ if (alphaKeys.Count == 0)
+ {
+ alphaKeys.Add(new GradientAlphaKey(1f, 0f));
+ alphaKeys.Add(new GradientAlphaKey(1f, 1f));
+ }
+
+ gradient.SetKeys(colorKeys.ToArray(), alphaKeys.ToArray());
+ return gradient;
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"[VectorParsing] Failed to parse Gradient from '{token}': {ex.Message}");
+ }
+
+ return null;
+ }
+
+ ///
+ /// Parses a JToken into a Gradient, returning a default gradient if parsing fails.
+ /// Added for ManageVFX refactoring.
+ ///
+ public static Gradient ParseGradientOrDefault(JToken token)
+ {
+ var result = ParseGradient(token);
+ if (result != null) return result;
+
+ // Return default white gradient
+ var gradient = new Gradient();
+ gradient.SetKeys(
+ new GradientColorKey[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) },
+ new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) }
+ );
+ return gradient;
+ }
+
+ ///
+ /// Parses a JToken into an AnimationCurve.
+ /// Supports formats:
+ /// - Constant: 1.0 (number)
+ /// - Simple: {start: 0.0, end: 1.0}
+ /// - Full: {keys: [{time: 0.0, value: 1.0, inTangent: 0.0, outTangent: 0.0}, ...]}
+ /// Added for ManageVFX refactoring.
+ ///
+ /// The JSON token to parse
+ /// The parsed AnimationCurve or null if parsing fails
+ public static AnimationCurve ParseAnimationCurve(JToken token)
+ {
+ if (token == null || token.Type == JTokenType.Null)
+ return null;
+
+ try
+ {
+ // Constant value: just a number
+ if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer)
+ {
+ return AnimationCurve.Constant(0f, 1f, token.ToObject());
+ }
+
+ if (token is JObject obj)
+ {
+ // Full format: {keys: [...]}
+ if (obj["keys"] is JArray keys)
+ {
+ AnimationCurve curve = new AnimationCurve();
+ foreach (var key in keys)
+ {
+ float time = key["time"]?.ToObject() ?? 0f;
+ float value = key["value"]?.ToObject() ?? 1f;
+ float inTangent = key["inTangent"]?.ToObject() ?? 0f;
+ float outTangent = key["outTangent"]?.ToObject() ?? 0f;
+ curve.AddKey(new Keyframe(time, value, inTangent, outTangent));
+ }
+ return curve;
+ }
+
+ // Simple format: {start: 0.0, end: 1.0} or {startValue: 0.0, endValue: 1.0}
+ if (obj.ContainsKey("start") || obj.ContainsKey("startValue") || obj.ContainsKey("end") || obj.ContainsKey("endValue"))
+ {
+ float startValue = obj["start"]?.ToObject() ?? obj["startValue"]?.ToObject() ?? 1f;
+ float endValue = obj["end"]?.ToObject() ?? obj["endValue"]?.ToObject() ?? 1f;
+ AnimationCurve curve = new AnimationCurve();
+ curve.AddKey(0f, startValue);
+ curve.AddKey(1f, endValue);
+ return curve;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"[VectorParsing] Failed to parse AnimationCurve from '{token}': {ex.Message}");
+ }
+
+ return null;
+ }
+
+ ///
+ /// Parses a JToken into an AnimationCurve, returning a constant curve if parsing fails.
+ /// Added for ManageVFX refactoring.
+ ///
+ /// The JSON token to parse
+ /// The constant value for the default curve
+ public static AnimationCurve ParseAnimationCurveOrDefault(JToken token, float defaultValue = 1f)
+ {
+ return ParseAnimationCurve(token) ?? AnimationCurve.Constant(0f, 1f, defaultValue);
+ }
+
///
/// Parses a JToken into a Rect.
/// Supports {x, y, width, height} format.
diff --git a/MCPForUnity/Editor/Tools/ManageVFX.cs b/MCPForUnity/Editor/Tools/ManageVFX.cs
new file mode 100644
index 000000000..c685b67f0
--- /dev/null
+++ b/MCPForUnity/Editor/Tools/ManageVFX.cs
@@ -0,0 +1,1702 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Helpers;
+using UnityEngine;
+using UnityEditor;
+
+#if UNITY_VFX_GRAPH //Please enable the symbol in the project settings for VisualEffectGraph to work
+using UnityEngine.VFX;
+#endif
+
+namespace MCPForUnity.Editor.Tools
+{
+ ///
+ /// Tool for managing Unity VFX components:
+ /// - ParticleSystem (legacy particle effects)
+ /// - Visual Effect Graph (modern GPU particles, currently only support HDRP, other SRPs may not work)
+ /// - LineRenderer (lines, bezier curves, shapes)
+ /// - TrailRenderer (motion trails)
+ /// - More to come based on demand and feedback!
+ ///
+ [McpForUnityTool("manage_vfx", AutoRegister = false)]
+ public static class ManageVFX
+ {
+ public static object HandleCommand(JObject @params)
+ {
+ string action = @params["action"]?.ToString();
+ if (string.IsNullOrEmpty(action))
+ {
+ return new { success = false, message = "Action is required" };
+ }
+
+ try
+ {
+ string actionLower = action.ToLowerInvariant();
+
+ // Route to appropriate handler based on action prefix
+ if (actionLower == "ping")
+ {
+ return new { success = true, tool = "manage_vfx", components = new[] { "ParticleSystem", "VisualEffect", "LineRenderer", "TrailRenderer" } };
+ }
+
+ // ParticleSystem actions (particle_*)
+ if (actionLower.StartsWith("particle_"))
+ {
+ return HandleParticleSystemAction(@params, actionLower.Substring(9));
+ }
+
+ // VFX Graph actions (vfx_*)
+ if (actionLower.StartsWith("vfx_"))
+ {
+ return HandleVFXGraphAction(@params, actionLower.Substring(4));
+ }
+
+ // LineRenderer actions (line_*)
+ if (actionLower.StartsWith("line_"))
+ {
+ return HandleLineRendererAction(@params, actionLower.Substring(5));
+ }
+
+ // TrailRenderer actions (trail_*)
+ if (actionLower.StartsWith("trail_"))
+ {
+ return HandleTrailRendererAction(@params, actionLower.Substring(6));
+ }
+
+ return new { success = false, message = $"Unknown action: {action}. Actions must be prefixed with: particle_, vfx_, line_, or trail_" };
+ }
+ catch (Exception ex)
+ {
+ return new { success = false, message = ex.Message, stackTrace = ex.StackTrace };
+ }
+ }
+
+ #region Common Helpers
+
+ // Parsing delegates for use with RendererHelpers
+ private static Color ParseColor(JToken token) => VectorParsing.ParseColorOrDefault(token);
+ private static Vector3 ParseVector3(JToken token) => VectorParsing.ParseVector3OrDefault(token);
+ private static Vector4 ParseVector4(JToken token) => VectorParsing.ParseVector4OrDefault(token);
+ private static Gradient ParseGradient(JToken token) => VectorParsing.ParseGradientOrDefault(token);
+ private static AnimationCurve ParseAnimationCurve(JToken token, float defaultValue = 1f)
+ => VectorParsing.ParseAnimationCurveOrDefault(token, defaultValue);
+
+ // Object resolution - delegates to ObjectResolver
+ private static GameObject FindTargetGameObject(JObject @params)
+ => ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
+ private static Material FindMaterialByPath(string path)
+ => ObjectResolver.ResolveMaterial(path);
+
+ #endregion
+
+ // ==================== PARTICLE SYSTEM ====================
+ #region ParticleSystem
+
+ private static object HandleParticleSystemAction(JObject @params, string action)
+ {
+ switch (action)
+ {
+ case "get_info": return ParticleGetInfo(@params);
+ case "set_main": return ParticleSetMain(@params);
+ case "set_emission": return ParticleSetEmission(@params);
+ case "set_shape": return ParticleSetShape(@params);
+ case "set_color_over_lifetime": return ParticleSetColorOverLifetime(@params);
+ case "set_size_over_lifetime": return ParticleSetSizeOverLifetime(@params);
+ case "set_velocity_over_lifetime": return ParticleSetVelocityOverLifetime(@params);
+ case "set_noise": return ParticleSetNoise(@params);
+ case "set_renderer": return ParticleSetRenderer(@params);
+ case "enable_module": return ParticleEnableModule(@params);
+ case "play": return ParticleControl(@params, "play");
+ case "stop": return ParticleControl(@params, "stop");
+ case "pause": return ParticleControl(@params, "pause");
+ case "restart": return ParticleControl(@params, "restart");
+ case "clear": return ParticleControl(@params, "clear");
+ case "add_burst": return ParticleAddBurst(@params);
+ case "clear_bursts": return ParticleClearBursts(@params);
+ default:
+ return new { success = false, message = $"Unknown particle action: {action}. Valid: get_info, set_main, set_emission, set_shape, set_color_over_lifetime, set_size_over_lifetime, set_velocity_over_lifetime, set_noise, set_renderer, enable_module, play, stop, pause, restart, clear, add_burst, clear_bursts" };
+ }
+ }
+
+ private static ParticleSystem FindParticleSystem(JObject @params)
+ {
+ GameObject go = FindTargetGameObject(@params);
+ return go?.GetComponent();
+ }
+
+ private static ParticleSystem.MinMaxCurve ParseMinMaxCurve(JToken token, float defaultValue = 1f)
+ {
+ if (token == null)
+ return new ParticleSystem.MinMaxCurve(defaultValue);
+
+ if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer)
+ {
+ return new ParticleSystem.MinMaxCurve(token.ToObject());
+ }
+
+ if (token is JObject obj)
+ {
+ string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "constant";
+
+ switch (mode)
+ {
+ case "constant":
+ float constant = obj["value"]?.ToObject() ?? defaultValue;
+ return new ParticleSystem.MinMaxCurve(constant);
+
+ case "random_between_constants":
+ case "two_constants":
+ float min = obj["min"]?.ToObject() ?? 0f;
+ float max = obj["max"]?.ToObject() ?? 1f;
+ return new ParticleSystem.MinMaxCurve(min, max);
+
+ case "curve":
+ AnimationCurve curve = ParseAnimationCurve(obj, defaultValue);
+ return new ParticleSystem.MinMaxCurve(obj["multiplier"]?.ToObject() ?? 1f, curve);
+
+ default:
+ return new ParticleSystem.MinMaxCurve(defaultValue);
+ }
+ }
+
+ return new ParticleSystem.MinMaxCurve(defaultValue);
+ }
+
+ private static ParticleSystem.MinMaxGradient ParseMinMaxGradient(JToken token)
+ {
+ if (token == null)
+ return new ParticleSystem.MinMaxGradient(Color.white);
+
+ if (token is JArray arr && arr.Count >= 3)
+ {
+ return new ParticleSystem.MinMaxGradient(ParseColor(arr));
+ }
+
+ if (token is JObject obj)
+ {
+ string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "color";
+
+ switch (mode)
+ {
+ case "color":
+ return new ParticleSystem.MinMaxGradient(ParseColor(obj["color"]));
+
+ case "two_colors":
+ Color colorMin = ParseColor(obj["colorMin"]);
+ Color colorMax = ParseColor(obj["colorMax"]);
+ return new ParticleSystem.MinMaxGradient(colorMin, colorMax);
+
+ case "gradient":
+ return new ParticleSystem.MinMaxGradient(ParseGradient(obj));
+
+ default:
+ return new ParticleSystem.MinMaxGradient(Color.white);
+ }
+ }
+
+ return new ParticleSystem.MinMaxGradient(Color.white);
+ }
+
+ private static object ParticleGetInfo(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null)
+ {
+ return new { success = false, message = "ParticleSystem not found" };
+ }
+
+ var main = ps.main;
+ var emission = ps.emission;
+ var shape = ps.shape;
+ var renderer = ps.GetComponent();
+
+ return new
+ {
+ success = true,
+ data = new
+ {
+ gameObject = ps.gameObject.name,
+ isPlaying = ps.isPlaying,
+ isPaused = ps.isPaused,
+ particleCount = ps.particleCount,
+ main = new
+ {
+ duration = main.duration,
+ looping = main.loop,
+ startLifetime = main.startLifetime.constant,
+ startSpeed = main.startSpeed.constant,
+ startSize = main.startSize.constant,
+ gravityModifier = main.gravityModifier.constant,
+ simulationSpace = main.simulationSpace.ToString(),
+ maxParticles = main.maxParticles
+ },
+ emission = new
+ {
+ enabled = emission.enabled,
+ rateOverTime = emission.rateOverTime.constant,
+ burstCount = emission.burstCount
+ },
+ shape = new
+ {
+ enabled = shape.enabled,
+ shapeType = shape.shapeType.ToString(),
+ radius = shape.radius,
+ angle = shape.angle
+ },
+ renderer = renderer != null ? new {
+ renderMode = renderer.renderMode.ToString(),
+ sortMode = renderer.sortMode.ToString(),
+ material = renderer.sharedMaterial?.name,
+ trailMaterial = renderer.trailMaterial?.name,
+ minParticleSize = renderer.minParticleSize,
+ maxParticleSize = renderer.maxParticleSize,
+ // Shadows & lighting
+ shadowCastingMode = renderer.shadowCastingMode.ToString(),
+ receiveShadows = renderer.receiveShadows,
+ lightProbeUsage = renderer.lightProbeUsage.ToString(),
+ reflectionProbeUsage = renderer.reflectionProbeUsage.ToString(),
+ // Sorting
+ sortingOrder = renderer.sortingOrder,
+ sortingLayerName = renderer.sortingLayerName,
+ renderingLayerMask = renderer.renderingLayerMask
+ } : null
+ }
+ };
+ }
+
+ private static object ParticleSetMain(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ Undo.RecordObject(ps, "Set ParticleSystem Main");
+ var main = ps.main;
+ var changes = new List();
+
+ if (@params["duration"] != null) { main.duration = @params["duration"].ToObject(); changes.Add("duration"); }
+ if (@params["looping"] != null) { main.loop = @params["looping"].ToObject(); changes.Add("looping"); }
+ if (@params["prewarm"] != null) { main.prewarm = @params["prewarm"].ToObject(); changes.Add("prewarm"); }
+ if (@params["startDelay"] != null) { main.startDelay = ParseMinMaxCurve(@params["startDelay"], 0f); changes.Add("startDelay"); }
+ if (@params["startLifetime"] != null) { main.startLifetime = ParseMinMaxCurve(@params["startLifetime"], 5f); changes.Add("startLifetime"); }
+ if (@params["startSpeed"] != null) { main.startSpeed = ParseMinMaxCurve(@params["startSpeed"], 5f); changes.Add("startSpeed"); }
+ if (@params["startSize"] != null) { main.startSize = ParseMinMaxCurve(@params["startSize"], 1f); changes.Add("startSize"); }
+ if (@params["startRotation"] != null) { main.startRotation = ParseMinMaxCurve(@params["startRotation"], 0f); changes.Add("startRotation"); }
+ if (@params["startColor"] != null) { main.startColor = ParseMinMaxGradient(@params["startColor"]); changes.Add("startColor"); }
+ if (@params["gravityModifier"] != null) { main.gravityModifier = ParseMinMaxCurve(@params["gravityModifier"], 0f); changes.Add("gravityModifier"); }
+ if (@params["simulationSpace"] != null && Enum.TryParse(@params["simulationSpace"].ToString(), true, out var simSpace)) { main.simulationSpace = simSpace; changes.Add("simulationSpace"); }
+ if (@params["scalingMode"] != null && Enum.TryParse(@params["scalingMode"].ToString(), true, out var scaleMode)) { main.scalingMode = scaleMode; changes.Add("scalingMode"); }
+ if (@params["playOnAwake"] != null) { main.playOnAwake = @params["playOnAwake"].ToObject(); changes.Add("playOnAwake"); }
+ if (@params["maxParticles"] != null) { main.maxParticles = @params["maxParticles"].ToObject(); changes.Add("maxParticles"); }
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
+ }
+
+ private static object ParticleSetEmission(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ Undo.RecordObject(ps, "Set ParticleSystem Emission");
+ var emission = ps.emission;
+ var changes = new List();
+
+ if (@params["enabled"] != null) { emission.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
+ if (@params["rateOverTime"] != null) { emission.rateOverTime = ParseMinMaxCurve(@params["rateOverTime"], 10f); changes.Add("rateOverTime"); }
+ if (@params["rateOverDistance"] != null) { emission.rateOverDistance = ParseMinMaxCurve(@params["rateOverDistance"], 0f); changes.Add("rateOverDistance"); }
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Updated emission: {string.Join(", ", changes)}" };
+ }
+
+ private static object ParticleSetShape(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ Undo.RecordObject(ps, "Set ParticleSystem Shape");
+ var shape = ps.shape;
+ var changes = new List();
+
+ if (@params["enabled"] != null) { shape.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
+ if (@params["shapeType"] != null && Enum.TryParse(@params["shapeType"].ToString(), true, out var shapeType)) { shape.shapeType = shapeType; changes.Add("shapeType"); }
+ if (@params["radius"] != null) { shape.radius = @params["radius"].ToObject(); changes.Add("radius"); }
+ if (@params["radiusThickness"] != null) { shape.radiusThickness = @params["radiusThickness"].ToObject(); changes.Add("radiusThickness"); }
+ if (@params["angle"] != null) { shape.angle = @params["angle"].ToObject(); changes.Add("angle"); }
+ if (@params["arc"] != null) { shape.arc = @params["arc"].ToObject(); changes.Add("arc"); }
+ if (@params["position"] != null) { shape.position = ParseVector3(@params["position"]); changes.Add("position"); }
+ if (@params["rotation"] != null) { shape.rotation = ParseVector3(@params["rotation"]); changes.Add("rotation"); }
+ if (@params["scale"] != null) { shape.scale = ParseVector3(@params["scale"]); changes.Add("scale"); }
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Updated shape: {string.Join(", ", changes)}" };
+ }
+
+ private static object ParticleSetColorOverLifetime(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ Undo.RecordObject(ps, "Set ParticleSystem Color Over Lifetime");
+ var col = ps.colorOverLifetime;
+ var changes = new List();
+
+ if (@params["enabled"] != null) { col.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
+ if (@params["color"] != null) { col.color = ParseMinMaxGradient(@params["color"]); changes.Add("color"); }
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
+ }
+
+ private static object ParticleSetSizeOverLifetime(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ Undo.RecordObject(ps, "Set ParticleSystem Size Over Lifetime");
+ var sol = ps.sizeOverLifetime;
+ var changes = new List();
+
+ // Auto-enable module if size properties are being set (unless explicitly disabled)
+ bool hasSizeProperty = @params["size"] != null || @params["sizeX"] != null ||
+ @params["sizeY"] != null || @params["sizeZ"] != null;
+ if (hasSizeProperty && @params["enabled"] == null && !sol.enabled)
+ {
+ sol.enabled = true;
+ changes.Add("enabled");
+ }
+ else if (@params["enabled"] != null)
+ {
+ sol.enabled = @params["enabled"].ToObject();
+ changes.Add("enabled");
+ }
+
+ if (@params["separateAxes"] != null) { sol.separateAxes = @params["separateAxes"].ToObject(); changes.Add("separateAxes"); }
+ if (@params["size"] != null) { sol.size = ParseMinMaxCurve(@params["size"], 1f); changes.Add("size"); }
+ if (@params["sizeX"] != null) { sol.x = ParseMinMaxCurve(@params["sizeX"], 1f); changes.Add("sizeX"); }
+ if (@params["sizeY"] != null) { sol.y = ParseMinMaxCurve(@params["sizeY"], 1f); changes.Add("sizeY"); }
+ if (@params["sizeZ"] != null) { sol.z = ParseMinMaxCurve(@params["sizeZ"], 1f); changes.Add("sizeZ"); }
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
+ }
+
+ private static object ParticleSetVelocityOverLifetime(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ Undo.RecordObject(ps, "Set ParticleSystem Velocity Over Lifetime");
+ var vol = ps.velocityOverLifetime;
+ var changes = new List();
+
+ if (@params["enabled"] != null) { vol.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
+ if (@params["space"] != null && Enum.TryParse(@params["space"].ToString(), true, out var space)) { vol.space = space; changes.Add("space"); }
+ if (@params["x"] != null) { vol.x = ParseMinMaxCurve(@params["x"], 0f); changes.Add("x"); }
+ if (@params["y"] != null) { vol.y = ParseMinMaxCurve(@params["y"], 0f); changes.Add("y"); }
+ if (@params["z"] != null) { vol.z = ParseMinMaxCurve(@params["z"], 0f); changes.Add("z"); }
+ if (@params["speedModifier"] != null) { vol.speedModifier = ParseMinMaxCurve(@params["speedModifier"], 1f); changes.Add("speedModifier"); }
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
+ }
+
+ private static object ParticleSetNoise(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ Undo.RecordObject(ps, "Set ParticleSystem Noise");
+ var noise = ps.noise;
+ var changes = new List();
+
+ if (@params["enabled"] != null) { noise.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
+ if (@params["strength"] != null) { noise.strength = ParseMinMaxCurve(@params["strength"], 1f); changes.Add("strength"); }
+ if (@params["frequency"] != null) { noise.frequency = @params["frequency"].ToObject(); changes.Add("frequency"); }
+ if (@params["scrollSpeed"] != null) { noise.scrollSpeed = ParseMinMaxCurve(@params["scrollSpeed"], 0f); changes.Add("scrollSpeed"); }
+ if (@params["damping"] != null) { noise.damping = @params["damping"].ToObject(); changes.Add("damping"); }
+ if (@params["octaveCount"] != null) { noise.octaveCount = @params["octaveCount"].ToObject(); changes.Add("octaveCount"); }
+ if (@params["quality"] != null && Enum.TryParse(@params["quality"].ToString(), true, out var quality)) { noise.quality = quality; changes.Add("quality"); }
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Updated noise: {string.Join(", ", changes)}" };
+ }
+
+ private static object ParticleSetRenderer(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ var renderer = ps.GetComponent();
+ if (renderer == null) return new { success = false, message = "ParticleSystemRenderer not found" };
+
+ Undo.RecordObject(renderer, "Set ParticleSystem Renderer");
+ var changes = new List();
+
+ // ParticleSystem-specific render modes
+ if (@params["renderMode"] != null && Enum.TryParse(@params["renderMode"].ToString(), true, out var renderMode)) { renderer.renderMode = renderMode; changes.Add("renderMode"); }
+ if (@params["sortMode"] != null && Enum.TryParse(@params["sortMode"].ToString(), true, out var sortMode)) { renderer.sortMode = sortMode; changes.Add("sortMode"); }
+
+ // Particle size limits
+ if (@params["minParticleSize"] != null) { renderer.minParticleSize = @params["minParticleSize"].ToObject(); changes.Add("minParticleSize"); }
+ if (@params["maxParticleSize"] != null) { renderer.maxParticleSize = @params["maxParticleSize"].ToObject(); changes.Add("maxParticleSize"); }
+
+ // Stretched billboard settings
+ if (@params["lengthScale"] != null) { renderer.lengthScale = @params["lengthScale"].ToObject(); changes.Add("lengthScale"); }
+ if (@params["velocityScale"] != null) { renderer.velocityScale = @params["velocityScale"].ToObject(); changes.Add("velocityScale"); }
+ if (@params["cameraVelocityScale"] != null) { renderer.cameraVelocityScale = @params["cameraVelocityScale"].ToObject(); changes.Add("cameraVelocityScale"); }
+ if (@params["normalDirection"] != null) { renderer.normalDirection = @params["normalDirection"].ToObject(); changes.Add("normalDirection"); }
+
+ // Alignment and pivot
+ if (@params["alignment"] != null && Enum.TryParse(@params["alignment"].ToString(), true, out var alignment)) { renderer.alignment = alignment; changes.Add("alignment"); }
+ if (@params["pivot"] != null) { renderer.pivot = ParseVector3(@params["pivot"]); changes.Add("pivot"); }
+ if (@params["flip"] != null) { renderer.flip = ParseVector3(@params["flip"]); changes.Add("flip"); }
+ if (@params["allowRoll"] != null) { renderer.allowRoll = @params["allowRoll"].ToObject(); changes.Add("allowRoll"); }
+
+ //special case for particle system renderer
+ if (@params["shadowBias"] != null) { renderer.shadowBias = @params["shadowBias"].ToObject(); changes.Add("shadowBias"); }
+
+ // Common Renderer properties (shadows, lighting, probes, sorting)
+ RendererHelpers.ApplyCommonRendererProperties(renderer, @params, changes);
+
+ // Material
+ if (@params["materialPath"] != null)
+ {
+ var findInst = new JObject { ["find"] = @params["materialPath"].ToString() };
+ Material mat = ManageGameObject.FindObjectByInstruction(findInst, typeof(Material)) as Material;
+ if (mat != null) { renderer.sharedMaterial = mat; changes.Add("material"); }
+ }
+ if (@params["trailMaterialPath"] != null)
+ {
+ var findInst = new JObject { ["find"] = @params["trailMaterialPath"].ToString() };
+ Material mat = ManageGameObject.FindObjectByInstruction(findInst, typeof(Material)) as Material;
+ if (mat != null) { renderer.trailMaterial = mat; changes.Add("trailMaterial"); }
+ }
+
+ EditorUtility.SetDirty(renderer);
+ return new { success = true, message = $"Updated renderer: {string.Join(", ", changes)}" };
+ }
+
+ private static object ParticleEnableModule(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ string moduleName = @params["module"]?.ToString()?.ToLowerInvariant();
+ bool enabled = @params["enabled"]?.ToObject() ?? true;
+
+ if (string.IsNullOrEmpty(moduleName)) return new { success = false, message = "Module name required" };
+
+ Undo.RecordObject(ps, $"Toggle {moduleName}");
+
+ switch (moduleName.Replace("_", ""))
+ {
+ case "emission": var em = ps.emission; em.enabled = enabled; break;
+ case "shape": var sh = ps.shape; sh.enabled = enabled; break;
+ case "coloroverlifetime": var col = ps.colorOverLifetime; col.enabled = enabled; break;
+ case "sizeoverlifetime": var sol = ps.sizeOverLifetime; sol.enabled = enabled; break;
+ case "velocityoverlifetime": var vol = ps.velocityOverLifetime; vol.enabled = enabled; break;
+ case "noise": var n = ps.noise; n.enabled = enabled; break;
+ case "collision": var coll = ps.collision; coll.enabled = enabled; break;
+ case "trails": var tr = ps.trails; tr.enabled = enabled; break;
+ case "lights": var li = ps.lights; li.enabled = enabled; break;
+ default: return new { success = false, message = $"Unknown module: {moduleName}" };
+ }
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Module '{moduleName}' {(enabled ? "enabled" : "disabled")}" };
+ }
+
+ private static object ParticleControl(JObject @params, string action)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ bool withChildren = @params["withChildren"]?.ToObject() ?? true;
+
+ switch (action)
+ {
+ case "play": ps.Play(withChildren); break;
+ case "stop": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmitting); break;
+ case "pause": ps.Pause(withChildren); break;
+ case "restart": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmittingAndClear); ps.Play(withChildren); break;
+ case "clear": ps.Clear(withChildren); break;
+ }
+
+ return new { success = true, message = $"ParticleSystem {action}" };
+ }
+
+ private static object ParticleAddBurst(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ Undo.RecordObject(ps, "Add Burst");
+ var emission = ps.emission;
+
+ float time = @params["time"]?.ToObject() ?? 0f;
+ short minCount = (short)(@params["minCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30);
+ short maxCount = (short)(@params["maxCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30);
+ int cycles = @params["cycles"]?.ToObject() ?? 1;
+ float interval = @params["interval"]?.ToObject() ?? 0.01f;
+
+ var burst = new ParticleSystem.Burst(time, minCount, maxCount, cycles, interval);
+ burst.probability = @params["probability"]?.ToObject() ?? 1f;
+
+ int idx = emission.burstCount;
+ var bursts = new ParticleSystem.Burst[idx + 1];
+ emission.GetBursts(bursts);
+ bursts[idx] = burst;
+ emission.SetBursts(bursts);
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Added burst at t={time}", burstIndex = idx };
+ }
+
+ private static object ParticleClearBursts(JObject @params)
+ {
+ ParticleSystem ps = FindParticleSystem(@params);
+ if (ps == null) return new { success = false, message = "ParticleSystem not found" };
+
+ Undo.RecordObject(ps, "Clear Bursts");
+ var emission = ps.emission;
+ int count = emission.burstCount;
+ emission.SetBursts(new ParticleSystem.Burst[0]);
+
+ EditorUtility.SetDirty(ps);
+ return new { success = true, message = $"Cleared {count} bursts" };
+ }
+
+ #endregion
+
+ // ==================== VFX GRAPH ====================
+ #region VFX Graph
+
+ private static object HandleVFXGraphAction(JObject @params, string action)
+ {
+#if !UNITY_VFX_GRAPH
+ return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
+#else
+ switch (action)
+ {
+ // Asset management
+ case "create_asset": return VFXCreateAsset(@params);
+ case "assign_asset": return VFXAssignAsset(@params);
+ case "list_templates": return VFXListTemplates(@params);
+ case "list_assets": return VFXListAssets(@params);
+
+ // Runtime parameter control
+ case "get_info": return VFXGetInfo(@params);
+ case "set_float": return VFXSetParameter(@params, (vfx, n, v) => vfx.SetFloat(n, v));
+ case "set_int": return VFXSetParameter(@params, (vfx, n, v) => vfx.SetInt(n, v));
+ case "set_bool": return VFXSetParameter(@params, (vfx, n, v) => vfx.SetBool(n, v));
+ case "set_vector2": return VFXSetVector(@params, 2);
+ case "set_vector3": return VFXSetVector(@params, 3);
+ case "set_vector4": return VFXSetVector(@params, 4);
+ case "set_color": return VFXSetColor(@params);
+ case "set_gradient": return VFXSetGradient(@params);
+ case "set_texture": return VFXSetTexture(@params);
+ case "set_mesh": return VFXSetMesh(@params);
+ case "set_curve": return VFXSetCurve(@params);
+ case "send_event": return VFXSendEvent(@params);
+ case "play": return VFXControl(@params, "play");
+ case "stop": return VFXControl(@params, "stop");
+ case "pause": return VFXControl(@params, "pause");
+ case "reinit": return VFXControl(@params, "reinit");
+ case "set_playback_speed": return VFXSetPlaybackSpeed(@params);
+ case "set_seed": return VFXSetSeed(@params);
+ default:
+ return new { success = false, message = $"Unknown vfx action: {action}. Valid: create_asset, assign_asset, list_templates, list_assets, get_info, set_float, set_int, set_bool, set_vector2/3/4, set_color, set_gradient, set_texture, set_mesh, set_curve, send_event, play, stop, pause, reinit, set_playback_speed, set_seed" };
+ }
+#endif
+ }
+
+#if UNITY_VFX_GRAPH
+ private static VisualEffect FindVisualEffect(JObject @params)
+ {
+ GameObject go = FindTargetGameObject(@params);
+ return go?.GetComponent();
+ }
+
+ ///
+ /// Creates a new VFX Graph asset file from a template
+ ///
+ private static object VFXCreateAsset(JObject @params)
+ {
+ string assetName = @params["assetName"]?.ToString();
+ string folderPath = @params["folderPath"]?.ToString() ?? "Assets/VFX";
+ string template = @params["template"]?.ToString() ?? "empty";
+
+ if (string.IsNullOrEmpty(assetName))
+ return new { success = false, message = "assetName is required" };
+
+ // Ensure folder exists
+ if (!AssetDatabase.IsValidFolder(folderPath))
+ {
+ string[] folders = folderPath.Split('/');
+ string currentPath = folders[0];
+ for (int i = 1; i < folders.Length; i++)
+ {
+ string newPath = currentPath + "/" + folders[i];
+ if (!AssetDatabase.IsValidFolder(newPath))
+ {
+ AssetDatabase.CreateFolder(currentPath, folders[i]);
+ }
+ currentPath = newPath;
+ }
+ }
+
+ string assetPath = $"{folderPath}/{assetName}.vfx";
+
+ // Check if asset already exists
+ if (AssetDatabase.LoadAssetAtPath(assetPath) != null)
+ {
+ bool overwrite = @params["overwrite"]?.ToObject() ?? false;
+ if (!overwrite)
+ return new { success = false, message = $"Asset already exists at {assetPath}. Set overwrite=true to replace." };
+ AssetDatabase.DeleteAsset(assetPath);
+ }
+
+ // Find and copy template
+ string templatePath = FindVFXTemplate(template);
+ UnityEngine.VFX.VisualEffectAsset newAsset = null;
+
+ if (!string.IsNullOrEmpty(templatePath) && System.IO.File.Exists(templatePath))
+ {
+ // templatePath is a full filesystem path, need to copy file directly
+ // Get the full destination path
+ string projectRoot = System.IO.Path.GetDirectoryName(Application.dataPath);
+ string fullDestPath = System.IO.Path.Combine(projectRoot, assetPath);
+
+ // Ensure directory exists
+ string destDir = System.IO.Path.GetDirectoryName(fullDestPath);
+ if (!System.IO.Directory.Exists(destDir))
+ System.IO.Directory.CreateDirectory(destDir);
+
+ // Copy the file
+ System.IO.File.Copy(templatePath, fullDestPath, true);
+ AssetDatabase.Refresh();
+ newAsset = AssetDatabase.LoadAssetAtPath(assetPath);
+ }
+ else
+ {
+ // Create empty VFX asset using reflection to access internal API
+ // Note: Develop in Progress, TODO:// Find authenticated way to create VFX asset
+ try
+ {
+ // Try to use VisualEffectAssetEditorUtility.CreateNewAsset if available
+ var utilityType = System.Type.GetType("UnityEditor.VFX.VisualEffectAssetEditorUtility, Unity.VisualEffectGraph.Editor");
+ if (utilityType != null)
+ {
+ var createMethod = utilityType.GetMethod("CreateNewAsset", BindingFlags.Public | BindingFlags.Static);
+ if (createMethod != null)
+ {
+ createMethod.Invoke(null, new object[] { assetPath });
+ AssetDatabase.Refresh();
+ newAsset = AssetDatabase.LoadAssetAtPath(assetPath);
+ }
+ }
+
+ // Fallback: Create a ScriptableObject-based asset
+ if (newAsset == null)
+ {
+ // Try direct creation via internal constructor
+ var resourceType = System.Type.GetType("UnityEditor.VFX.VisualEffectResource, Unity.VisualEffectGraph.Editor");
+ if (resourceType != null)
+ {
+ var createMethod = resourceType.GetMethod("CreateNewAsset", BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic);
+ if (createMethod != null)
+ {
+ var resource = createMethod.Invoke(null, new object[] { assetPath });
+ AssetDatabase.Refresh();
+ newAsset = AssetDatabase.LoadAssetAtPath(assetPath);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ return new { success = false, message = $"Failed to create VFX asset: {ex.Message}" };
+ }
+ }
+
+ if (newAsset == null)
+ {
+ return new { success = false, message = "Failed to create VFX asset. Try using a template from list_templates." };
+ }
+
+ return new
+ {
+ success = true,
+ message = $"Created VFX asset: {assetPath}",
+ data = new
+ {
+ assetPath = assetPath,
+ assetName = newAsset.name,
+ template = template
+ }
+ };
+ }
+
+ ///
+ /// Finds VFX template path by name
+ ///
+ private static string FindVFXTemplate(string templateName)
+ {
+ // Get the actual filesystem path for the VFX Graph package using PackageManager API
+ var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph");
+
+ var searchPaths = new List();
+
+ if (packageInfo != null)
+ {
+ // Use the resolved path from PackageManager (handles Library/PackageCache paths)
+ searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates"));
+ searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples"));
+ }
+
+ // Also search project-local paths
+ searchPaths.Add("Assets/VFX/Templates");
+
+ string[] templatePatterns = new[]
+ {
+ $"{templateName}.vfx",
+ $"VFX{templateName}.vfx",
+ $"Simple{templateName}.vfx",
+ $"{templateName}VFX.vfx"
+ };
+
+ foreach (string basePath in searchPaths)
+ {
+ if (!System.IO.Directory.Exists(basePath)) continue;
+
+ foreach (string pattern in templatePatterns)
+ {
+ string[] files = System.IO.Directory.GetFiles(basePath, pattern, System.IO.SearchOption.AllDirectories);
+ if (files.Length > 0)
+ return files[0];
+ }
+
+ // Also search by partial match
+ try
+ {
+ string[] allVfxFiles = System.IO.Directory.GetFiles(basePath, "*.vfx", System.IO.SearchOption.AllDirectories);
+ foreach (string file in allVfxFiles)
+ {
+ if (System.IO.Path.GetFileNameWithoutExtension(file).ToLower().Contains(templateName.ToLower()))
+ return file;
+ }
+ }
+ catch { }
+ }
+
+ // Search in project assets
+ string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset " + templateName);
+ if (guids.Length > 0)
+ {
+ return AssetDatabase.GUIDToAssetPath(guids[0]);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Assigns a VFX asset to a VisualEffect component
+ ///
+ private static object VFXAssignAsset(JObject @params)
+ {
+ VisualEffect vfx = FindVisualEffect(@params);
+ if (vfx == null) return new { success = false, message = "VisualEffect component not found" };
+
+ string assetPath = @params["assetPath"]?.ToString();
+ if (string.IsNullOrEmpty(assetPath))
+ return new { success = false, message = "assetPath is required" };
+
+ // Normalize path
+ if (!assetPath.StartsWith("Assets/") && !assetPath.StartsWith("Packages/"))
+ assetPath = "Assets/" + assetPath;
+ if (!assetPath.EndsWith(".vfx"))
+ assetPath += ".vfx";
+
+ var asset = AssetDatabase.LoadAssetAtPath(assetPath);
+ if (asset == null)
+ {
+ // Try searching by name
+ string searchName = System.IO.Path.GetFileNameWithoutExtension(assetPath);
+ string[] guids = AssetDatabase.FindAssets($"t:VisualEffectAsset {searchName}");
+ if (guids.Length > 0)
+ {
+ assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
+ asset = AssetDatabase.LoadAssetAtPath(assetPath);
+ }
+ }
+
+ if (asset == null)
+ return new { success = false, message = $"VFX asset not found: {assetPath}" };
+
+ Undo.RecordObject(vfx, "Assign VFX Asset");
+ vfx.visualEffectAsset = asset;
+ EditorUtility.SetDirty(vfx);
+
+ return new
+ {
+ success = true,
+ message = $"Assigned VFX asset '{asset.name}' to {vfx.gameObject.name}",
+ data = new
+ {
+ gameObject = vfx.gameObject.name,
+ assetName = asset.name,
+ assetPath = assetPath
+ }
+ };
+ }
+
+ ///
+ /// Lists available VFX templates
+ ///
+ private static object VFXListTemplates(JObject @params)
+ {
+ var templates = new List