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(); + + // 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"); + searchPaths.Add("Assets/VFX"); + + // Precompute normalized package path for comparison + string normalizedPackagePath = null; + if (packageInfo != null) + { + normalizedPackagePath = packageInfo.resolvedPath.Replace("\\", "/"); + } + + // Precompute the Assets base path for converting absolute paths to project-relative + string assetsBasePath = Application.dataPath.Replace("\\", "/"); + + foreach (string basePath in searchPaths) + { + if (!System.IO.Directory.Exists(basePath)) continue; + + try + { + string[] vfxFiles = System.IO.Directory.GetFiles(basePath, "*.vfx", System.IO.SearchOption.AllDirectories); + foreach (string file in vfxFiles) + { + string absolutePath = file.Replace("\\", "/"); + string name = System.IO.Path.GetFileNameWithoutExtension(file); + bool isPackage = normalizedPackagePath != null && absolutePath.StartsWith(normalizedPackagePath); + + // Convert absolute path to project-relative path + string projectRelativePath; + if (isPackage) + { + // For package paths, convert to Packages/... format + projectRelativePath = "Packages/" + packageInfo.name + absolutePath.Substring(normalizedPackagePath.Length); + } + else if (absolutePath.StartsWith(assetsBasePath)) + { + // For project assets, convert to Assets/... format + projectRelativePath = "Assets" + absolutePath.Substring(assetsBasePath.Length); + } + else + { + // Fallback: use the absolute path if we can't determine the relative path + projectRelativePath = absolutePath; + } + + templates.Add(new { name = name, path = projectRelativePath, source = isPackage ? "package" : "project" }); + } + } + catch { } + } + + // Also search project assets + string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset"); + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + if (!templates.Any(t => ((dynamic)t).path == path)) + { + string name = System.IO.Path.GetFileNameWithoutExtension(path); + templates.Add(new { name = name, path = path, source = "project" }); + } + } + + return new + { + success = true, + data = new + { + count = templates.Count, + templates = templates + } + }; + } + + /// + /// Lists all VFX assets in the project + /// + private static object VFXListAssets(JObject @params) + { + string searchFolder = @params["folder"]?.ToString(); + string searchPattern = @params["search"]?.ToString(); + + string filter = "t:VisualEffectAsset"; + if (!string.IsNullOrEmpty(searchPattern)) + filter += " " + searchPattern; + + string[] guids; + if (!string.IsNullOrEmpty(searchFolder)) + guids = AssetDatabase.FindAssets(filter, new[] { searchFolder }); + else + guids = AssetDatabase.FindAssets(filter); + + var assets = new List(); + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + var asset = AssetDatabase.LoadAssetAtPath(path); + if (asset != null) + { + assets.Add(new + { + name = asset.name, + path = path, + guid = guid + }); + } + } + + return new + { + success = true, + data = new + { + count = assets.Count, + assets = assets + } + }; + } + + private static object VFXGetInfo(JObject @params) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + return new + { + success = true, + data = new + { + gameObject = vfx.gameObject.name, + assetName = vfx.visualEffectAsset?.name ?? "None", + aliveParticleCount = vfx.aliveParticleCount, + culled = vfx.culled, + pause = vfx.pause, + playRate = vfx.playRate, + startSeed = vfx.startSeed + } + }; + } + + private static object VFXSetParameter(JObject @params, Action setter) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + string param = @params["parameter"]?.ToString(); + if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; + + JToken valueToken = @params["value"]; + if (valueToken == null) return new { success = false, message = "Value required" }; + + Undo.RecordObject(vfx, $"Set VFX {param}"); + T value = valueToken.ToObject(); + setter(vfx, param, value); + EditorUtility.SetDirty(vfx); + + return new { success = true, message = $"Set {param} = {value}" }; + } + + private static object VFXSetVector(JObject @params, int dims) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + string param = @params["parameter"]?.ToString(); + if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; + + Vector4 vec = ParseVector4(@params["value"]); + Undo.RecordObject(vfx, $"Set VFX {param}"); + + switch (dims) + { + case 2: vfx.SetVector2(param, new Vector2(vec.x, vec.y)); break; + case 3: vfx.SetVector3(param, new Vector3(vec.x, vec.y, vec.z)); break; + case 4: vfx.SetVector4(param, vec); break; + } + + EditorUtility.SetDirty(vfx); + return new { success = true, message = $"Set {param}" }; + } + + private static object VFXSetColor(JObject @params) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + string param = @params["parameter"]?.ToString(); + if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; + + Color color = ParseColor(@params["value"]); + Undo.RecordObject(vfx, $"Set VFX Color {param}"); + vfx.SetVector4(param, new Vector4(color.r, color.g, color.b, color.a)); + EditorUtility.SetDirty(vfx); + + return new { success = true, message = $"Set color {param}" }; + } + + private static object VFXSetGradient(JObject @params) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + string param = @params["parameter"]?.ToString(); + if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; + + Gradient gradient = ParseGradient(@params["gradient"]); + Undo.RecordObject(vfx, $"Set VFX Gradient {param}"); + vfx.SetGradient(param, gradient); + EditorUtility.SetDirty(vfx); + + return new { success = true, message = $"Set gradient {param}" }; + } + + private static object VFXSetTexture(JObject @params) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + string param = @params["parameter"]?.ToString(); + string path = @params["texturePath"]?.ToString(); + if (string.IsNullOrEmpty(param) || string.IsNullOrEmpty(path)) return new { success = false, message = "Parameter and texturePath required" }; + + var findInst = new JObject { ["find"] = path }; + Texture tex = ManageGameObject.FindObjectByInstruction(findInst, typeof(Texture)) as Texture; + if (tex == null) return new { success = false, message = $"Texture not found: {path}" }; + + Undo.RecordObject(vfx, $"Set VFX Texture {param}"); + vfx.SetTexture(param, tex); + EditorUtility.SetDirty(vfx); + + return new { success = true, message = $"Set texture {param} = {tex.name}" }; + } + + private static object VFXSetMesh(JObject @params) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + string param = @params["parameter"]?.ToString(); + string path = @params["meshPath"]?.ToString(); + if (string.IsNullOrEmpty(param) || string.IsNullOrEmpty(path)) return new { success = false, message = "Parameter and meshPath required" }; + + var findInst = new JObject { ["find"] = path }; + Mesh mesh = ManageGameObject.FindObjectByInstruction(findInst, typeof(Mesh)) as Mesh; + if (mesh == null) return new { success = false, message = $"Mesh not found: {path}" }; + + Undo.RecordObject(vfx, $"Set VFX Mesh {param}"); + vfx.SetMesh(param, mesh); + EditorUtility.SetDirty(vfx); + + return new { success = true, message = $"Set mesh {param} = {mesh.name}" }; + } + + private static object VFXSetCurve(JObject @params) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + string param = @params["parameter"]?.ToString(); + if (string.IsNullOrEmpty(param)) return new { success = false, message = "Parameter name required" }; + + AnimationCurve curve = ParseAnimationCurve(@params["curve"], 1f); + Undo.RecordObject(vfx, $"Set VFX Curve {param}"); + vfx.SetAnimationCurve(param, curve); + EditorUtility.SetDirty(vfx); + + return new { success = true, message = $"Set curve {param}" }; + } + + private static object VFXSendEvent(JObject @params) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + string eventName = @params["eventName"]?.ToString(); + if (string.IsNullOrEmpty(eventName)) return new { success = false, message = "Event name required" }; + + VFXEventAttribute attr = vfx.CreateVFXEventAttribute(); + if (@params["position"] != null) attr.SetVector3("position", ParseVector3(@params["position"])); + if (@params["velocity"] != null) attr.SetVector3("velocity", ParseVector3(@params["velocity"])); + if (@params["color"] != null) { var c = ParseColor(@params["color"]); attr.SetVector3("color", new Vector3(c.r, c.g, c.b)); } + if (@params["size"] != null) attr.SetFloat("size", @params["size"].ToObject()); + if (@params["lifetime"] != null) attr.SetFloat("lifetime", @params["lifetime"].ToObject()); + + vfx.SendEvent(eventName, attr); + return new { success = true, message = $"Sent event '{eventName}'" }; + } + + private static object VFXControl(JObject @params, string action) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + switch (action) + { + case "play": vfx.Play(); break; + case "stop": vfx.Stop(); break; + case "pause": vfx.pause = !vfx.pause; break; + case "reinit": vfx.Reinit(); break; + } + + return new { success = true, message = $"VFX {action}", isPaused = vfx.pause }; + } + + private static object VFXSetPlaybackSpeed(JObject @params) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + float rate = @params["playRate"]?.ToObject() ?? 1f; + Undo.RecordObject(vfx, "Set VFX Play Rate"); + vfx.playRate = rate; + EditorUtility.SetDirty(vfx); + + return new { success = true, message = $"Set play rate = {rate}" }; + } + + private static object VFXSetSeed(JObject @params) + { + VisualEffect vfx = FindVisualEffect(@params); + if (vfx == null) return new { success = false, message = "VisualEffect not found" }; + + uint seed = @params["seed"]?.ToObject() ?? 0; + bool resetOnPlay = @params["resetSeedOnPlay"]?.ToObject() ?? true; + + Undo.RecordObject(vfx, "Set VFX Seed"); + vfx.startSeed = seed; + vfx.resetSeedOnPlay = resetOnPlay; + EditorUtility.SetDirty(vfx); + + return new { success = true, message = $"Set seed = {seed}" }; + } +#endif + + #endregion + + // ==================== LINE RENDERER ==================== + #region LineRenderer + + private static object HandleLineRendererAction(JObject @params, string action) + { + switch (action) + { + case "get_info": return LineGetInfo(@params); + case "set_positions": return LineSetPositions(@params); + case "add_position": return LineAddPosition(@params); + case "set_position": return LineSetPosition(@params); + case "set_width": return LineSetWidth(@params); + case "set_color": return LineSetColor(@params); + case "set_material": return LineSetMaterial(@params); + case "set_properties": return LineSetProperties(@params); + case "clear": return LineClear(@params); + case "create_line": return LineCreateLine(@params); + case "create_circle": return LineCreateCircle(@params); + case "create_arc": return LineCreateArc(@params); + case "create_bezier": return LineCreateBezier(@params); + default: + return new { success = false, message = $"Unknown line action: {action}. Valid: get_info, set_positions, add_position, set_position, set_width, set_color, set_material, set_properties, clear, create_line, create_circle, create_arc, create_bezier" }; + } + } + + private static LineRenderer FindLineRenderer(JObject @params) + { + GameObject go = FindTargetGameObject(@params); + return go?.GetComponent(); + } + + private static object LineGetInfo(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + var positions = new Vector3[lr.positionCount]; + lr.GetPositions(positions); + + return new + { + success = true, + data = new + { + gameObject = lr.gameObject.name, + positionCount = lr.positionCount, + positions = positions.Select(p => new { x = p.x, y = p.y, z = p.z }).ToArray(), + startWidth = lr.startWidth, + endWidth = lr.endWidth, + loop = lr.loop, + useWorldSpace = lr.useWorldSpace, + alignment = lr.alignment.ToString(), + textureMode = lr.textureMode.ToString(), + numCornerVertices = lr.numCornerVertices, + numCapVertices = lr.numCapVertices, + generateLightingData = lr.generateLightingData, + material = lr.sharedMaterial?.name, + // Shadows & lighting + shadowCastingMode = lr.shadowCastingMode.ToString(), + receiveShadows = lr.receiveShadows, + lightProbeUsage = lr.lightProbeUsage.ToString(), + reflectionProbeUsage = lr.reflectionProbeUsage.ToString(), + // Sorting + sortingOrder = lr.sortingOrder, + sortingLayerName = lr.sortingLayerName, + renderingLayerMask = lr.renderingLayerMask + } + }; + } + + private static object LineSetPositions(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + JArray posArr = @params["positions"] as JArray; + if (posArr == null) return new { success = false, message = "Positions array required" }; + + var positions = posArr.Select(p => ParseVector3(p)).ToArray(); + + Undo.RecordObject(lr, "Set Line Positions"); + lr.positionCount = positions.Length; + lr.SetPositions(positions); + EditorUtility.SetDirty(lr); + + return new { success = true, message = $"Set {positions.Length} positions" }; + } + + private static object LineAddPosition(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + Vector3 pos = ParseVector3(@params["position"]); + + Undo.RecordObject(lr, "Add Line Position"); + int idx = lr.positionCount; + lr.positionCount = idx + 1; + lr.SetPosition(idx, pos); + EditorUtility.SetDirty(lr); + + return new { success = true, message = $"Added position at index {idx}", index = idx }; + } + + private static object LineSetPosition(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + int index = @params["index"]?.ToObject() ?? -1; + if (index < 0 || index >= lr.positionCount) return new { success = false, message = $"Invalid index {index}" }; + + Vector3 pos = ParseVector3(@params["position"]); + + Undo.RecordObject(lr, "Set Line Position"); + lr.SetPosition(index, pos); + EditorUtility.SetDirty(lr); + + return new { success = true, message = $"Set position at index {index}" }; + } + + private static object LineSetWidth(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + Undo.RecordObject(lr, "Set Line Width"); + var changes = new List(); + + RendererHelpers.ApplyWidthProperties(@params, changes, + v => lr.startWidth = v, v => lr.endWidth = v, + v => lr.widthCurve = v, v => lr.widthMultiplier = v, + ParseAnimationCurve); + + EditorUtility.SetDirty(lr); + return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; + } + + private static object LineSetColor(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + Undo.RecordObject(lr, "Set Line Color"); + var changes = new List(); + + RendererHelpers.ApplyColorProperties(@params, changes, + v => lr.startColor = v, v => lr.endColor = v, + v => lr.colorGradient = v, + ParseColor, ParseGradient, fadeEndAlpha: false); + + EditorUtility.SetDirty(lr); + return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; + } + + private static object LineSetMaterial(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + return RendererHelpers.SetRendererMaterial(lr, @params, "Set Line Material", FindMaterialByPath); + } + + private static object LineSetProperties(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + Undo.RecordObject(lr, "Set Line Properties"); + var changes = new List(); + + // Line-specific properties + RendererHelpers.ApplyLineTrailProperties(@params, changes, + v => lr.loop = v, v => lr.useWorldSpace = v, + v => lr.numCornerVertices = v, v => lr.numCapVertices = v, + v => lr.alignment = v, v => lr.textureMode = v, + v => lr.generateLightingData = v); + + // Common Renderer properties (shadows, lighting, probes, sorting) + RendererHelpers.ApplyCommonRendererProperties(lr, @params, changes); + + EditorUtility.SetDirty(lr); + return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; + } + + private static object LineClear(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + int count = lr.positionCount; + Undo.RecordObject(lr, "Clear Line"); + lr.positionCount = 0; + EditorUtility.SetDirty(lr); + + return new { success = true, message = $"Cleared {count} positions" }; + } + + private static object LineCreateLine(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + Vector3 start = ParseVector3(@params["start"]); + Vector3 end = ParseVector3(@params["end"]); + + Undo.RecordObject(lr, "Create Line"); + lr.positionCount = 2; + lr.SetPosition(0, start); + lr.SetPosition(1, end); + EditorUtility.SetDirty(lr); + + return new { success = true, message = "Created line" }; + } + + private static object LineCreateCircle(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + Vector3 center = ParseVector3(@params["center"]); + float radius = @params["radius"]?.ToObject() ?? 1f; + int segments = @params["segments"]?.ToObject() ?? 32; + Vector3 normal = @params["normal"] != null ? ParseVector3(@params["normal"]).normalized : Vector3.up; + + Vector3 right = Vector3.Cross(normal, Vector3.forward); + if (right.sqrMagnitude < 0.001f) right = Vector3.Cross(normal, Vector3.up); + right = right.normalized; + Vector3 forward = Vector3.Cross(right, normal).normalized; + + Undo.RecordObject(lr, "Create Circle"); + lr.positionCount = segments; + lr.loop = true; + + for (int i = 0; i < segments; i++) + { + float angle = (float)i / segments * Mathf.PI * 2f; + Vector3 point = center + (right * Mathf.Cos(angle) + forward * Mathf.Sin(angle)) * radius; + lr.SetPosition(i, point); + } + + EditorUtility.SetDirty(lr); + return new { success = true, message = $"Created circle with {segments} segments" }; + } + + private static object LineCreateArc(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + Vector3 center = ParseVector3(@params["center"]); + float radius = @params["radius"]?.ToObject() ?? 1f; + float startAngle = (@params["startAngle"]?.ToObject() ?? 0f) * Mathf.Deg2Rad; + float endAngle = (@params["endAngle"]?.ToObject() ?? 180f) * Mathf.Deg2Rad; + int segments = @params["segments"]?.ToObject() ?? 16; + Vector3 normal = @params["normal"] != null ? ParseVector3(@params["normal"]).normalized : Vector3.up; + + Vector3 right = Vector3.Cross(normal, Vector3.forward); + if (right.sqrMagnitude < 0.001f) right = Vector3.Cross(normal, Vector3.up); + right = right.normalized; + Vector3 forward = Vector3.Cross(right, normal).normalized; + + Undo.RecordObject(lr, "Create Arc"); + lr.positionCount = segments + 1; + lr.loop = false; + + for (int i = 0; i <= segments; i++) + { + float t = (float)i / segments; + float angle = Mathf.Lerp(startAngle, endAngle, t); + Vector3 point = center + (right * Mathf.Cos(angle) + forward * Mathf.Sin(angle)) * radius; + lr.SetPosition(i, point); + } + + EditorUtility.SetDirty(lr); + return new { success = true, message = $"Created arc with {segments} segments" }; + } + + private static object LineCreateBezier(JObject @params) + { + LineRenderer lr = FindLineRenderer(@params); + if (lr == null) return new { success = false, message = "LineRenderer not found" }; + + Vector3 start = ParseVector3(@params["start"]); + Vector3 end = ParseVector3(@params["end"]); + Vector3 cp1 = ParseVector3(@params["controlPoint1"] ?? @params["control1"]); + Vector3 cp2 = @params["controlPoint2"] != null || @params["control2"] != null + ? ParseVector3(@params["controlPoint2"] ?? @params["control2"]) + : cp1; + int segments = @params["segments"]?.ToObject() ?? 32; + bool isQuadratic = @params["controlPoint2"] == null && @params["control2"] == null; + + Undo.RecordObject(lr, "Create Bezier"); + lr.positionCount = segments + 1; + lr.loop = false; + + for (int i = 0; i <= segments; i++) + { + float t = (float)i / segments; + Vector3 point; + + if (isQuadratic) + { + float u = 1 - t; + point = u * u * start + 2 * u * t * cp1 + t * t * end; + } + else + { + float u = 1 - t; + point = u * u * u * start + 3 * u * u * t * cp1 + 3 * u * t * t * cp2 + t * t * t * end; + } + + lr.SetPosition(i, point); + } + + EditorUtility.SetDirty(lr); + return new { success = true, message = $"Created {(isQuadratic ? "quadratic" : "cubic")} Bezier" }; + } + + #endregion + + // ==================== TRAIL RENDERER ==================== + #region TrailRenderer + + private static object HandleTrailRendererAction(JObject @params, string action) + { + switch (action) + { + case "get_info": return TrailGetInfo(@params); + case "set_time": return TrailSetTime(@params); + case "set_width": return TrailSetWidth(@params); + case "set_color": return TrailSetColor(@params); + case "set_material": return TrailSetMaterial(@params); + case "set_properties": return TrailSetProperties(@params); + case "clear": return TrailClear(@params); + case "emit": return TrailEmit(@params); + default: + return new { success = false, message = $"Unknown trail action: {action}. Valid: get_info, set_time, set_width, set_color, set_material, set_properties, clear, emit" }; + } + } + + private static TrailRenderer FindTrailRenderer(JObject @params) + { + GameObject go = FindTargetGameObject(@params); + return go?.GetComponent(); + } + + private static object TrailGetInfo(JObject @params) + { + TrailRenderer tr = FindTrailRenderer(@params); + if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + + return new + { + success = true, + data = new + { + gameObject = tr.gameObject.name, + time = tr.time, + startWidth = tr.startWidth, + endWidth = tr.endWidth, + minVertexDistance = tr.minVertexDistance, + emitting = tr.emitting, + autodestruct = tr.autodestruct, + positionCount = tr.positionCount, + alignment = tr.alignment.ToString(), + textureMode = tr.textureMode.ToString(), + numCornerVertices = tr.numCornerVertices, + numCapVertices = tr.numCapVertices, + generateLightingData = tr.generateLightingData, + material = tr.sharedMaterial?.name, + // Shadows & lighting + shadowCastingMode = tr.shadowCastingMode.ToString(), + receiveShadows = tr.receiveShadows, + lightProbeUsage = tr.lightProbeUsage.ToString(), + reflectionProbeUsage = tr.reflectionProbeUsage.ToString(), + // Sorting + sortingOrder = tr.sortingOrder, + sortingLayerName = tr.sortingLayerName, + renderingLayerMask = tr.renderingLayerMask + } + }; + } + + private static object TrailSetTime(JObject @params) + { + TrailRenderer tr = FindTrailRenderer(@params); + if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + + float time = @params["time"]?.ToObject() ?? 5f; + + Undo.RecordObject(tr, "Set Trail Time"); + tr.time = time; + EditorUtility.SetDirty(tr); + + return new { success = true, message = $"Set trail time to {time}s" }; + } + + private static object TrailSetWidth(JObject @params) + { + TrailRenderer tr = FindTrailRenderer(@params); + if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + + Undo.RecordObject(tr, "Set Trail Width"); + var changes = new List(); + + RendererHelpers.ApplyWidthProperties(@params, changes, + v => tr.startWidth = v, v => tr.endWidth = v, + v => tr.widthCurve = v, v => tr.widthMultiplier = v, + ParseAnimationCurve); + + EditorUtility.SetDirty(tr); + return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; + } + + private static object TrailSetColor(JObject @params) + { + TrailRenderer tr = FindTrailRenderer(@params); + if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + + Undo.RecordObject(tr, "Set Trail Color"); + var changes = new List(); + + RendererHelpers.ApplyColorProperties(@params, changes, + v => tr.startColor = v, v => tr.endColor = v, + v => tr.colorGradient = v, + ParseColor, ParseGradient, fadeEndAlpha: true); + + EditorUtility.SetDirty(tr); + return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; + } + + private static object TrailSetMaterial(JObject @params) + { + TrailRenderer tr = FindTrailRenderer(@params); + return RendererHelpers.SetRendererMaterial(tr, @params, "Set Trail Material", FindMaterialByPath); + } + + private static object TrailSetProperties(JObject @params) + { + TrailRenderer tr = FindTrailRenderer(@params); + if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + + Undo.RecordObject(tr, "Set Trail Properties"); + var changes = new List(); + + // Trail-specific properties (not shared with LineRenderer) + if (@params["minVertexDistance"] != null) { tr.minVertexDistance = @params["minVertexDistance"].ToObject(); changes.Add("minVertexDistance"); } + if (@params["autodestruct"] != null) { tr.autodestruct = @params["autodestruct"].ToObject(); changes.Add("autodestruct"); } + if (@params["emitting"] != null) { tr.emitting = @params["emitting"].ToObject(); changes.Add("emitting"); } + + // Shared Line/Trail properties + RendererHelpers.ApplyLineTrailProperties(@params, changes, + null, null, // Trail doesn't have loop or useWorldSpace + v => tr.numCornerVertices = v, v => tr.numCapVertices = v, + v => tr.alignment = v, v => tr.textureMode = v, + v => tr.generateLightingData = v); + + // Common Renderer properties (shadows, lighting, probes, sorting) + RendererHelpers.ApplyCommonRendererProperties(tr, @params, changes); + + EditorUtility.SetDirty(tr); + return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; + } + + private static object TrailClear(JObject @params) + { + TrailRenderer tr = FindTrailRenderer(@params); + if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + + Undo.RecordObject(tr, "Clear Trail"); + tr.Clear(); + return new { success = true, message = "Trail cleared" }; + } + + private static object TrailEmit(JObject @params) + { + TrailRenderer tr = FindTrailRenderer(@params); + if (tr == null) return new { success = false, message = "TrailRenderer not found" }; + +#if UNITY_2021_1_OR_NEWER + Vector3 pos = ParseVector3(@params["position"]); + tr.AddPosition(pos); + return new { success = true, message = $"Emitted at ({pos.x}, {pos.y}, {pos.z})" }; +#else + return new { success = false, message = "AddPosition requires Unity 2021.1+" }; +#endif + } + + #endregion + } +} diff --git a/MCPForUnity/Editor/Tools/ManageVFX.cs.meta b/MCPForUnity/Editor/Tools/ManageVFX.cs.meta new file mode 100644 index 000000000..e1fb8116c --- /dev/null +++ b/MCPForUnity/Editor/Tools/ManageVFX.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: a8f3d2c1e9b74f6a8c5d0e2f1a3b4c5d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + + diff --git a/Server/src/services/tools/manage_vfx.py b/Server/src/services/tools/manage_vfx.py new file mode 100644 index 000000000..567fa9853 --- /dev/null +++ b/Server/src/services/tools/manage_vfx.py @@ -0,0 +1,627 @@ +from typing import Annotated, Any, Literal + +from fastmcp import Context +from mcp.types import ToolAnnotations + +from services.registry import mcp_for_unity_tool +from services.tools import get_unity_instance_from_context +from transport.unity_transport import send_with_unity_instance +from transport.legacy.unity_connection import async_send_command_with_retry + +# All possible actions grouped by component type +PARTICLE_ACTIONS = [ + "particle_get_info", "particle_set_main", "particle_set_emission", "particle_set_shape", + "particle_set_color_over_lifetime", "particle_set_size_over_lifetime", + "particle_set_velocity_over_lifetime", "particle_set_noise", "particle_set_renderer", + "particle_enable_module", "particle_play", "particle_stop", "particle_pause", + "particle_restart", "particle_clear", "particle_add_burst", "particle_clear_bursts" +] + +VFX_ACTIONS = [ + # Asset management + "vfx_create_asset", "vfx_assign_asset", "vfx_list_templates", "vfx_list_assets", + # Runtime control + "vfx_get_info", "vfx_set_float", "vfx_set_int", "vfx_set_bool", + "vfx_set_vector2", "vfx_set_vector3", "vfx_set_vector4", "vfx_set_color", + "vfx_set_gradient", "vfx_set_texture", "vfx_set_mesh", "vfx_set_curve", + "vfx_send_event", "vfx_play", "vfx_stop", "vfx_pause", "vfx_reinit", + "vfx_set_playback_speed", "vfx_set_seed" +] + +LINE_ACTIONS = [ + "line_get_info", "line_set_positions", "line_add_position", "line_set_position", + "line_set_width", "line_set_color", "line_set_material", "line_set_properties", + "line_clear", "line_create_line", "line_create_circle", "line_create_arc", "line_create_bezier" +] + +TRAIL_ACTIONS = [ + "trail_get_info", "trail_set_time", "trail_set_width", "trail_set_color", + "trail_set_material", "trail_set_properties", "trail_clear", "trail_emit" +] + +ALL_ACTIONS = ["ping"] + PARTICLE_ACTIONS + VFX_ACTIONS + LINE_ACTIONS + TRAIL_ACTIONS + + +@mcp_for_unity_tool( + description="""Unified VFX management for Unity visual effects components. + +Each action prefix requires a specific component on the target GameObject: +- `particle_*` actions require **ParticleSystem** component +- `vfx_*` actions require **VisualEffect** component (+ com.unity.visualeffectgraph package) +- `line_*` actions require **LineRenderer** component +- `trail_*` actions require **TrailRenderer** component + +**If the component doesn't exist, the action will FAIL +Before using this tool, either: +1. Use `manage_gameobject` with `action="get_components"` to check if component exists +2. Use `manage_gameobject` with `action="add_component", component_name="ParticleSystem"` (or LineRenderer/TrailRenderer/VisualEffect) to add the component first +3. Assign material to the component beforehand to avoid empty effects + +**TARGETING:** +Use `target` parameter to specify the GameObject: +- By name: `target="Fire"` (finds first GameObject named "Fire") +- By path: `target="Effects/Fire"` with `search_method="by_path"` +- By instance ID: `target="12345"` with `search_method="by_id"` (most reliable) +- By tag: `target="Player"` with `search_method="by_tag"` + +**Component Types & Action Prefixes:** +- `particle_*` - ParticleSystem (legacy particle effects) +- `vfx_*` - Visual Effect Graph (modern GPU particles, requires com.unity.visualeffectgraph) +- `line_*` - LineRenderer (lines, curves, shapes) +- `trail_*` - TrailRenderer (motion trails) + +**ParticleSystem Actions (particle_*):** +- particle_get_info: Get particle system info +- particle_set_main: Set main module (duration, looping, startLifetime, startSpeed, startSize, startColor, gravityModifier, maxParticles) +- particle_set_emission: Set emission (rateOverTime, rateOverDistance) +- particle_set_shape: Set shape (shapeType, radius, angle, arc, position, rotation, scale) +- particle_set_color_over_lifetime, particle_set_size_over_lifetime, particle_set_velocity_over_lifetime +- particle_set_noise: Set noise (strength, frequency, scrollSpeed) +- particle_set_renderer: Set renderer (renderMode, material) +- particle_enable_module: Enable/disable modules +- particle_play/stop/pause/restart/clear: Playback control +- particle_add_burst, particle_clear_bursts: Burst management + +**VFX Graph Actions (vfx_*):** +- **Asset Management:** + - vfx_create_asset: Create a new VFX Graph asset file (requires: assetName, optional: folderPath, template, overwrite) + - vfx_assign_asset: Assign a VFX asset to a VisualEffect component (requires: target, assetPath) + - vfx_list_templates: List available VFX templates in project and packages + - vfx_list_assets: List all VFX assets in project (optional: folder, search) +- **Runtime Control:** + - vfx_get_info: Get VFX info + - vfx_set_float/int/bool: Set exposed parameters + - vfx_set_vector2/vector3/vector4: Set vector parameters + - vfx_set_color, vfx_set_gradient: Set color/gradient parameters + - vfx_set_texture, vfx_set_mesh: Set asset parameters + - vfx_set_curve: Set animation curve + - vfx_send_event: Send events with attributes (position, velocity, color, size, lifetime) + - vfx_play/stop/pause/reinit: Playback control + - vfx_set_playback_speed, vfx_set_seed + +**LineRenderer Actions (line_*):** +- line_get_info: Get line info +- line_set_positions: Set all positions +- line_add_position, line_set_position: Modify positions +- line_set_width: Set width (uniform, start/end, curve) +- line_set_color: Set color (uniform, gradient) +- line_set_material, line_set_properties +- line_clear: Clear positions +- line_create_line: Create simple line +- line_create_circle: Create circle +- line_create_arc: Create arc +- line_create_bezier: Create Bezier curve + +**TrailRenderer Actions (trail_*):** +- trail_get_info: Get trail info +- trail_set_time: Set trail duration +- trail_set_width, trail_set_color, trail_set_material, trail_set_properties +- trail_clear: Clear trail +- trail_emit: Emit point (Unity 2021.1+)""", + annotations=ToolAnnotations( + title="Manage VFX", + destructiveHint=True, + ), +) +async def manage_vfx( + ctx: Context, + action: Annotated[str, "Action to perform. Use prefix: particle_, vfx_, line_, or trail_"], + + # Target specification (common) - REQUIRED for most actions + # Using str | None to accept any string format + target: Annotated[str | None, "Target GameObject with the VFX component. Use name (e.g. 'Fire'), path ('Effects/Fire'), instance ID, or tag. The GameObject MUST have the required component (ParticleSystem/VisualEffect/LineRenderer/TrailRenderer) for the action prefix."] = None, + search_method: Annotated[ + Literal["by_id", "by_name", "by_path", "by_tag", "by_layer"] | None, + "How to find target: by_name (default), by_path (hierarchy path), by_id (instance ID - most reliable), by_tag, by_layer" + ] = None, + + # === PARTICLE SYSTEM PARAMETERS === + # Main module - All use Any to accept string coercion from MCP clients + duration: Annotated[Any, "[Particle] Duration in seconds (number or string)"] = None, + looping: Annotated[Any, "[Particle] Whether to loop (bool or string 'true'/'false')"] = None, + prewarm: Annotated[Any, "[Particle] Prewarm the system (bool or string)"] = None, + start_delay: Annotated[Any, "[Particle] Start delay (number or MinMaxCurve dict)"] = None, + start_lifetime: Annotated[Any, "[Particle] Particle lifetime (number or MinMaxCurve dict)"] = None, + start_speed: Annotated[Any, "[Particle] Initial speed (number or MinMaxCurve dict)"] = None, + start_size: Annotated[Any, "[Particle] Initial size (number or MinMaxCurve dict)"] = None, + start_rotation: Annotated[Any, "[Particle] Initial rotation (number or MinMaxCurve dict)"] = None, + start_color: Annotated[Any, "[Particle/VFX] Start color [r,g,b,a] (array, dict, or JSON string)"] = None, + gravity_modifier: Annotated[Any, "[Particle] Gravity multiplier (number or MinMaxCurve dict)"] = None, + simulation_space: Annotated[Literal["Local", "World", "Custom"] | None, "[Particle] Simulation space"] = None, + scaling_mode: Annotated[Literal["Hierarchy", "Local", "Shape"] | None, "[Particle] Scaling mode"] = None, + play_on_awake: Annotated[Any, "[Particle] Play on awake (bool or string)"] = None, + max_particles: Annotated[Any, "[Particle] Maximum particles (integer or string)"] = None, + + # Emission + rate_over_time: Annotated[Any, "[Particle] Emission rate over time (number or MinMaxCurve dict)"] = None, + rate_over_distance: Annotated[Any, "[Particle] Emission rate over distance (number or MinMaxCurve dict)"] = None, + + # Shape + shape_type: Annotated[Literal["Sphere", "Hemisphere", "Cone", "Box", "Circle", "Edge", "Donut"] | None, "[Particle] Shape type"] = None, + radius: Annotated[Any, "[Particle/Line] Shape radius (number or string)"] = None, + radius_thickness: Annotated[Any, "[Particle] Radius thickness 0-1 (number or string)"] = None, + angle: Annotated[Any, "[Particle] Cone angle (number or string)"] = None, + arc: Annotated[Any, "[Particle] Arc angle (number or string)"] = None, + + # Noise + strength: Annotated[Any, "[Particle] Noise strength (number or MinMaxCurve dict)"] = None, + frequency: Annotated[Any, "[Particle] Noise frequency (number or string)"] = None, + scroll_speed: Annotated[Any, "[Particle] Noise scroll speed (number or MinMaxCurve dict)"] = None, + damping: Annotated[Any, "[Particle] Noise damping (bool or string)"] = None, + octave_count: Annotated[Any, "[Particle] Noise octaves 1-4 (integer or string)"] = None, + quality: Annotated[Literal["Low", "Medium", "High"] | None, "[Particle] Noise quality"] = None, + + # Module control + module: Annotated[str | None, "[Particle] Module name to enable/disable"] = None, + enabled: Annotated[Any, "[Particle] Enable/disable module (bool or string)"] = None, + + # Burst + time: Annotated[Any, "[Particle/Trail] Burst time or trail duration (number or string)"] = None, + count: Annotated[Any, "[Particle] Burst count (integer or string)"] = None, + min_count: Annotated[Any, "[Particle] Min burst count (integer or string)"] = None, + max_count: Annotated[Any, "[Particle] Max burst count (integer or string)"] = None, + cycles: Annotated[Any, "[Particle] Burst cycles (integer or string)"] = None, + interval: Annotated[Any, "[Particle] Burst interval (number or string)"] = None, + probability: Annotated[Any, "[Particle] Burst probability 0-1 (number or string)"] = None, + + # Playback + with_children: Annotated[Any, "[Particle] Apply to children (bool or string)"] = None, + + # === VFX GRAPH PARAMETERS === + # Asset management + asset_name: Annotated[str | None, "[VFX] Name for new VFX asset (without .vfx extension)"] = None, + folder_path: Annotated[str | None, "[VFX] Folder path for new asset (default: Assets/VFX)"] = None, + template: Annotated[str | None, "[VFX] Template name for new asset (use vfx_list_templates to see available)"] = None, + asset_path: Annotated[str | None, "[VFX] Path to VFX asset to assign (e.g. Assets/VFX/MyEffect.vfx)"] = None, + overwrite: Annotated[Any, "[VFX] Overwrite existing asset (bool or string)"] = None, + folder: Annotated[str | None, "[VFX] Folder to search for assets (for vfx_list_assets)"] = None, + search: Annotated[str | None, "[VFX] Search pattern for assets (for vfx_list_assets)"] = None, + + # Runtime parameters + parameter: Annotated[str | None, "[VFX] Exposed parameter name"] = None, + value: Annotated[Any, "[VFX] Parameter value (number, bool, array, or string)"] = None, + texture_path: Annotated[str | None, "[VFX] Texture asset path"] = None, + mesh_path: Annotated[str | None, "[VFX] Mesh asset path"] = None, + gradient: Annotated[Any, "[VFX/Line/Trail] Gradient {colorKeys, alphaKeys} or {startColor, endColor} (dict or JSON string)"] = None, + curve: Annotated[Any, "[VFX] Animation curve keys or {startValue, endValue} (array, dict, or JSON string)"] = None, + event_name: Annotated[str | None, "[VFX] Event name to send"] = None, + velocity: Annotated[Any, "[VFX] Event velocity [x,y,z] (array or JSON string)"] = None, + size: Annotated[Any, "[VFX] Event size (number or string)"] = None, + lifetime: Annotated[Any, "[VFX] Event lifetime (number or string)"] = None, + play_rate: Annotated[Any, "[VFX] Playback speed multiplier (number or string)"] = None, + seed: Annotated[Any, "[VFX] Random seed (integer or string)"] = None, + reset_seed_on_play: Annotated[Any, "[VFX] Reset seed on play (bool or string)"] = None, + + # === LINE/TRAIL RENDERER PARAMETERS === + positions: Annotated[Any, "[Line] Positions [[x,y,z], ...] (array or JSON string)"] = None, + position: Annotated[Any, "[Line/Trail] Single position [x,y,z] (array or JSON string)"] = None, + index: Annotated[Any, "[Line] Position index (integer or string)"] = None, + + # Width + width: Annotated[Any, "[Line/Trail] Uniform width (number or string)"] = None, + start_width: Annotated[Any, "[Line/Trail] Start width (number or string)"] = None, + end_width: Annotated[Any, "[Line/Trail] End width (number or string)"] = None, + width_curve: Annotated[Any, "[Line/Trail] Width curve (number or dict)"] = None, + width_multiplier: Annotated[Any, "[Line/Trail] Width multiplier (number or string)"] = None, + + # Color + color: Annotated[Any, "[Line/Trail/VFX] Color [r,g,b,a] (array or JSON string)"] = None, + start_color_line: Annotated[Any, "[Line/Trail] Start color (array or JSON string)"] = None, + end_color: Annotated[Any, "[Line/Trail] End color (array or JSON string)"] = None, + + # Material & properties + material_path: Annotated[str | None, "[Particle/Line/Trail] Material asset path"] = None, + trail_material_path: Annotated[str | None, "[Particle] Trail material asset path"] = None, + loop: Annotated[Any, "[Line] Connect end to start (bool or string)"] = None, + use_world_space: Annotated[Any, "[Line] Use world space (bool or string)"] = None, + num_corner_vertices: Annotated[Any, "[Line/Trail] Corner vertices (integer or string)"] = None, + num_cap_vertices: Annotated[Any, "[Line/Trail] Cap vertices (integer or string)"] = None, + alignment: Annotated[Literal["View", "Local", "TransformZ"] | None, "[Line/Trail] Alignment"] = None, + texture_mode: Annotated[Literal["Stretch", "Tile", "DistributePerSegment", "RepeatPerSegment"] | None, "[Line/Trail] Texture mode"] = None, + generate_lighting_data: Annotated[Any, "[Line/Trail] Generate lighting data for GI (bool or string)"] = None, + sorting_order: Annotated[Any, "[Line/Trail/Particle] Sorting order (integer or string)"] = None, + sorting_layer_name: Annotated[str | None, "[Renderer] Sorting layer name"] = None, + sorting_layer_id: Annotated[Any, "[Renderer] Sorting layer ID (integer or string)"] = None, + render_mode: Annotated[str | None, "[Particle] Render mode (Billboard, Stretch, HorizontalBillboard, VerticalBillboard, Mesh, None)"] = None, + sort_mode: Annotated[str | None, "[Particle] Sort mode (None, Distance, OldestInFront, YoungestInFront, Depth)"] = None, + + # === RENDERER COMMON PROPERTIES (Shadows, Lighting, Probes) === + shadow_casting_mode: Annotated[Literal["Off", "On", "TwoSided", "ShadowsOnly"] | None, "[Renderer] Shadow casting mode"] = None, + receive_shadows: Annotated[Any, "[Renderer] Receive shadows (bool or string)"] = None, + shadow_bias: Annotated[Any, "[Renderer] Shadow bias (number or string)"] = None, + light_probe_usage: Annotated[Literal["Off", "BlendProbes", "UseProxyVolume", "CustomProvided"] | None, "[Renderer] Light probe usage mode"] = None, + reflection_probe_usage: Annotated[Literal["Off", "BlendProbes", "BlendProbesAndSkybox", "Simple"] | None, "[Renderer] Reflection probe usage mode"] = None, + motion_vector_generation_mode: Annotated[Literal["Camera", "Object", "ForceNoMotion"] | None, "[Renderer] Motion vector generation mode"] = None, + rendering_layer_mask: Annotated[Any, "[Renderer] Rendering layer mask for SRP (integer or string)"] = None, + + # === PARTICLE RENDERER SPECIFIC === + min_particle_size: Annotated[Any, "[Particle] Min particle size relative to viewport (number or string)"] = None, + max_particle_size: Annotated[Any, "[Particle] Max particle size relative to viewport (number or string)"] = None, + length_scale: Annotated[Any, "[Particle] Length scale for stretched billboard (number or string)"] = None, + velocity_scale: Annotated[Any, "[Particle] Velocity scale for stretched billboard (number or string)"] = None, + camera_velocity_scale: Annotated[Any, "[Particle] Camera velocity scale for stretched billboard (number or string)"] = None, + normal_direction: Annotated[Any, "[Particle] Normal direction 0-1 (number or string)"] = None, + pivot: Annotated[Any, "[Particle] Pivot offset [x,y,z] (array or JSON string)"] = None, + flip: Annotated[Any, "[Particle] Flip [x,y,z] (array or JSON string)"] = None, + allow_roll: Annotated[Any, "[Particle] Allow roll for mesh particles (bool or string)"] = None, + + # Shape creation (line_create_*) + start: Annotated[Any, "[Line] Start point [x,y,z] (array or JSON string)"] = None, + end: Annotated[Any, "[Line] End point [x,y,z] (array or JSON string)"] = None, + center: Annotated[Any, "[Line] Circle/arc center [x,y,z] (array or JSON string)"] = None, + segments: Annotated[Any, "[Line] Number of segments (integer or string)"] = None, + normal: Annotated[Any, "[Line] Normal direction [x,y,z] (array or JSON string)"] = None, + start_angle: Annotated[Any, "[Line] Arc start angle degrees (number or string)"] = None, + end_angle: Annotated[Any, "[Line] Arc end angle degrees (number or string)"] = None, + control_point1: Annotated[Any, "[Line] Bezier control point 1 (array or JSON string)"] = None, + control_point2: Annotated[Any, "[Line] Bezier control point 2 (cubic) (array or JSON string)"] = None, + + # Trail specific + min_vertex_distance: Annotated[Any, "[Trail] Min vertex distance (number or string)"] = None, + autodestruct: Annotated[Any, "[Trail] Destroy when finished (bool or string)"] = None, + emitting: Annotated[Any, "[Trail] Is emitting (bool or string)"] = None, + + # Common vector params for shape/velocity + x: Annotated[Any, "[Particle] Velocity X (number or MinMaxCurve dict)"] = None, + y: Annotated[Any, "[Particle] Velocity Y (number or MinMaxCurve dict)"] = None, + z: Annotated[Any, "[Particle] Velocity Z (number or MinMaxCurve dict)"] = None, + speed_modifier: Annotated[Any, "[Particle] Speed modifier (number or MinMaxCurve dict)"] = None, + space: Annotated[Literal["Local", "World"] | None, "[Particle] Velocity space"] = None, + separate_axes: Annotated[Any, "[Particle] Separate XYZ axes (bool or string)"] = None, + size_over_lifetime: Annotated[Any, "[Particle] Size over lifetime (number or MinMaxCurve dict)"] = None, + size_x: Annotated[Any, "[Particle] Size X (number or MinMaxCurve dict)"] = None, + size_y: Annotated[Any, "[Particle] Size Y (number or MinMaxCurve dict)"] = None, + size_z: Annotated[Any, "[Particle] Size Z (number or MinMaxCurve dict)"] = None, + +) -> dict[str, Any]: + """Unified VFX management tool.""" + + # Normalize action to lowercase to match Unity-side behavior + action_normalized = action.lower() + + # Validate action against known actions using normalized value + if action_normalized not in ALL_ACTIONS: + # Provide helpful error with closest matches by prefix + prefix = action_normalized.split("_")[0] + "_" if "_" in action_normalized else "" + available_by_prefix = { + "particle_": PARTICLE_ACTIONS, + "vfx_": VFX_ACTIONS, + "line_": LINE_ACTIONS, + "trail_": TRAIL_ACTIONS, + } + suggestions = available_by_prefix.get(prefix, []) + if suggestions: + return { + "success": False, + "message": f"Unknown action '{action}'. Available {prefix}* actions: {', '.join(suggestions)}", + } + else: + return { + "success": False, + "message": ( + f"Unknown action '{action}'. Use prefixes: " + "particle_*, vfx_*, line_*, trail_*. Run with action='ping' to test connection." + ), + } + + unity_instance = get_unity_instance_from_context(ctx) + + # Build parameters dict with normalized action to stay consistent with Unity + params_dict: dict[str, Any] = {"action": action_normalized} + + # Target + if target is not None: + params_dict["target"] = target + if search_method is not None: + params_dict["searchMethod"] = search_method + + # === PARTICLE SYSTEM === + # Pass through all values - C# side handles parsing (ParseColor, ParseVector3, ParseMinMaxCurve, ToObject) + if duration is not None: + params_dict["duration"] = duration + if looping is not None: + params_dict["looping"] = looping + if prewarm is not None: + params_dict["prewarm"] = prewarm + if start_delay is not None: + params_dict["startDelay"] = start_delay + if start_lifetime is not None: + params_dict["startLifetime"] = start_lifetime + if start_speed is not None: + params_dict["startSpeed"] = start_speed + if start_size is not None: + params_dict["startSize"] = start_size + if start_rotation is not None: + params_dict["startRotation"] = start_rotation + if start_color is not None: + params_dict["startColor"] = start_color + if gravity_modifier is not None: + params_dict["gravityModifier"] = gravity_modifier + if simulation_space is not None: + params_dict["simulationSpace"] = simulation_space + if scaling_mode is not None: + params_dict["scalingMode"] = scaling_mode + if play_on_awake is not None: + params_dict["playOnAwake"] = play_on_awake + if max_particles is not None: + params_dict["maxParticles"] = max_particles + + # Emission + if rate_over_time is not None: + params_dict["rateOverTime"] = rate_over_time + if rate_over_distance is not None: + params_dict["rateOverDistance"] = rate_over_distance + + # Shape + if shape_type is not None: + params_dict["shapeType"] = shape_type + if radius is not None: + params_dict["radius"] = radius + if radius_thickness is not None: + params_dict["radiusThickness"] = radius_thickness + if angle is not None: + params_dict["angle"] = angle + if arc is not None: + params_dict["arc"] = arc + + # Noise + if strength is not None: + params_dict["strength"] = strength + if frequency is not None: + params_dict["frequency"] = frequency + if scroll_speed is not None: + params_dict["scrollSpeed"] = scroll_speed + if damping is not None: + params_dict["damping"] = damping + if octave_count is not None: + params_dict["octaveCount"] = octave_count + if quality is not None: + params_dict["quality"] = quality + + # Module + if module is not None: + params_dict["module"] = module + if enabled is not None: + params_dict["enabled"] = enabled + + # Burst + if time is not None: + params_dict["time"] = time + if count is not None: + params_dict["count"] = count + if min_count is not None: + params_dict["minCount"] = min_count + if max_count is not None: + params_dict["maxCount"] = max_count + if cycles is not None: + params_dict["cycles"] = cycles + if interval is not None: + params_dict["interval"] = interval + if probability is not None: + params_dict["probability"] = probability + + # Playback + if with_children is not None: + params_dict["withChildren"] = with_children + + # === VFX GRAPH === + # Asset management parameters + if asset_name is not None: + params_dict["assetName"] = asset_name + if folder_path is not None: + params_dict["folderPath"] = folder_path + if template is not None: + params_dict["template"] = template + if asset_path is not None: + params_dict["assetPath"] = asset_path + if overwrite is not None: + params_dict["overwrite"] = overwrite + if folder is not None: + params_dict["folder"] = folder + if search is not None: + params_dict["search"] = search + + # Runtime parameters + if parameter is not None: + params_dict["parameter"] = parameter + if value is not None: + params_dict["value"] = value + if texture_path is not None: + params_dict["texturePath"] = texture_path + if mesh_path is not None: + params_dict["meshPath"] = mesh_path + if gradient is not None: + params_dict["gradient"] = gradient + if curve is not None: + params_dict["curve"] = curve + if event_name is not None: + params_dict["eventName"] = event_name + if velocity is not None: + params_dict["velocity"] = velocity + if size is not None: + params_dict["size"] = size + if lifetime is not None: + params_dict["lifetime"] = lifetime + if play_rate is not None: + params_dict["playRate"] = play_rate + if seed is not None: + params_dict["seed"] = seed + if reset_seed_on_play is not None: + params_dict["resetSeedOnPlay"] = reset_seed_on_play + + # === LINE/TRAIL RENDERER === + if positions is not None: + params_dict["positions"] = positions + if position is not None: + params_dict["position"] = position + if index is not None: + params_dict["index"] = index + + # Width + if width is not None: + params_dict["width"] = width + if start_width is not None: + params_dict["startWidth"] = start_width + if end_width is not None: + params_dict["endWidth"] = end_width + if width_curve is not None: + params_dict["widthCurve"] = width_curve + if width_multiplier is not None: + params_dict["widthMultiplier"] = width_multiplier + + # Color + if color is not None: + params_dict["color"] = color + if start_color_line is not None: + params_dict["startColor"] = start_color_line + if end_color is not None: + params_dict["endColor"] = end_color + + # Material & properties + if material_path is not None: + params_dict["materialPath"] = material_path + if trail_material_path is not None: + params_dict["trailMaterialPath"] = trail_material_path + if loop is not None: + params_dict["loop"] = loop + if use_world_space is not None: + params_dict["useWorldSpace"] = use_world_space + if num_corner_vertices is not None: + params_dict["numCornerVertices"] = num_corner_vertices + if num_cap_vertices is not None: + params_dict["numCapVertices"] = num_cap_vertices + if alignment is not None: + params_dict["alignment"] = alignment + if texture_mode is not None: + params_dict["textureMode"] = texture_mode + if generate_lighting_data is not None: + params_dict["generateLightingData"] = generate_lighting_data + if sorting_order is not None: + params_dict["sortingOrder"] = sorting_order + if sorting_layer_name is not None: + params_dict["sortingLayerName"] = sorting_layer_name + if sorting_layer_id is not None: + params_dict["sortingLayerID"] = sorting_layer_id + if render_mode is not None: + params_dict["renderMode"] = render_mode + if sort_mode is not None: + params_dict["sortMode"] = sort_mode + + # Renderer common properties (shadows, lighting, probes) + if shadow_casting_mode is not None: + params_dict["shadowCastingMode"] = shadow_casting_mode + if receive_shadows is not None: + params_dict["receiveShadows"] = receive_shadows + if shadow_bias is not None: + params_dict["shadowBias"] = shadow_bias + if light_probe_usage is not None: + params_dict["lightProbeUsage"] = light_probe_usage + if reflection_probe_usage is not None: + params_dict["reflectionProbeUsage"] = reflection_probe_usage + if motion_vector_generation_mode is not None: + params_dict["motionVectorGenerationMode"] = motion_vector_generation_mode + if rendering_layer_mask is not None: + params_dict["renderingLayerMask"] = rendering_layer_mask + + # Particle renderer specific + if min_particle_size is not None: + params_dict["minParticleSize"] = min_particle_size + if max_particle_size is not None: + params_dict["maxParticleSize"] = max_particle_size + if length_scale is not None: + params_dict["lengthScale"] = length_scale + if velocity_scale is not None: + params_dict["velocityScale"] = velocity_scale + if camera_velocity_scale is not None: + params_dict["cameraVelocityScale"] = camera_velocity_scale + if normal_direction is not None: + params_dict["normalDirection"] = normal_direction + if pivot is not None: + params_dict["pivot"] = pivot + if flip is not None: + params_dict["flip"] = flip + if allow_roll is not None: + params_dict["allowRoll"] = allow_roll + + # Shape creation + if start is not None: + params_dict["start"] = start + if end is not None: + params_dict["end"] = end + if center is not None: + params_dict["center"] = center + if segments is not None: + params_dict["segments"] = segments + if normal is not None: + params_dict["normal"] = normal + if start_angle is not None: + params_dict["startAngle"] = start_angle + if end_angle is not None: + params_dict["endAngle"] = end_angle + if control_point1 is not None: + params_dict["controlPoint1"] = control_point1 + if control_point2 is not None: + params_dict["controlPoint2"] = control_point2 + + # Trail specific + if min_vertex_distance is not None: + params_dict["minVertexDistance"] = min_vertex_distance + if autodestruct is not None: + params_dict["autodestruct"] = autodestruct + if emitting is not None: + params_dict["emitting"] = emitting + + # Velocity/size axes + if x is not None: + params_dict["x"] = x + if y is not None: + params_dict["y"] = y + if z is not None: + params_dict["z"] = z + if speed_modifier is not None: + params_dict["speedModifier"] = speed_modifier + if space is not None: + params_dict["space"] = space + if separate_axes is not None: + params_dict["separateAxes"] = separate_axes + if size_over_lifetime is not None: + params_dict["size"] = size_over_lifetime + if size_x is not None: + params_dict["sizeX"] = size_x + if size_y is not None: + params_dict["sizeY"] = size_y + if size_z is not None: + params_dict["sizeZ"] = size_z + + # Remove None values + params_dict = {k: v for k, v in params_dict.items() if v is not None} + + # Send to Unity + result = await send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "manage_vfx", + params_dict, + ) + + return result if isinstance(result, dict) else {"success": False, "message": str(result)} diff --git a/Server/src/services/tools/utils.py b/Server/src/services/tools/utils.py index 32f7a23ad..4d7ef907b 100644 --- a/Server/src/services/tools/utils.py +++ b/Server/src/services/tools/utils.py @@ -77,6 +77,23 @@ def coerce_int(value: Any, default: int | None = None) -> int | None: return default +def coerce_float(value: Any, default: float | None = None) -> float | None: + """Attempt to coerce a loosely-typed value to a float-like number.""" + if value is None: + return default + try: + # Treat booleans as invalid numeric input instead of coercing to 0/1. + if isinstance(value, bool): + return default + if isinstance(value, (int, float)): + return float(value) + s = str(value).strip() + if s.lower() in ("", "none", "null"): + return default + return float(s) + except (TypeError, ValueError): + return default + def normalize_properties(value: Any) -> tuple[dict[str, Any] | None, str | None]: """ Robustly normalize a properties parameter to a dict.