[FEATURE]: Manage VFX function (#520)
Feature: Add ManageVFX Add ManageVFX to the current function, support: 1. Modify LineRender, TrailRender properties 2. Modify particle system (legacy) properties 3. Modify VisualEffectGraph and use templates (does not support edit graph yet) * Update Server/src/services/tools/utils.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update Server/src/services/tools/manage_vfx.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update ManageVFX.cs Added 6 helper methods : ApplyCommonRendererProperties - Handles shadows, lighting, probes, sorting, rendering layer (used by ParticleSetRenderer, LineSetProperties, TrailSetProperties) GetCommonRendererInfo - Returns common renderer info for GetInfo methods ApplyWidthProperties - Generic width handling (used by LineSetWidth, TrailSetWidth) ApplyColorProperties - Generic color handling with fadeEndAlpha option (used by LineSetColor, TrailSetColor) SetRendererMaterial - Generic material assignment (used by LineSetMaterial, TrailSetMaterial) ApplyLineTrailProperties - Shared Line/Trail properties like alignment, textureMode, numCornerVertices (used by LineSetProperties, TrailSetProperties) * Optimize the code structure Declutter the redundant files by adding RendererHelpers and VectorParsing.cs * Minor Fixes Minor Fixes based on AI-feedback * Fixes --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>main
parent
22e52664cd
commit
d285c8d936
|
|
@ -0,0 +1,168 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnity.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class for common Renderer property operations.
|
||||
/// Used by ManageVFX for ParticleSystem, LineRenderer, and TrailRenderer components.
|
||||
/// </summary>
|
||||
public static class RendererHelpers
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Applies common Renderer properties (shadows, lighting, probes, sorting, rendering layer).
|
||||
/// Used by ParticleSetRenderer, LineSetProperties, TrailSetProperties.
|
||||
/// </summary>
|
||||
public static void ApplyCommonRendererProperties(Renderer renderer, JObject @params, List<string> changes)
|
||||
{
|
||||
// Shadows
|
||||
if (@params["shadowCastingMode"] != null && Enum.TryParse<UnityEngine.Rendering.ShadowCastingMode>(@params["shadowCastingMode"].ToString(), true, out var shadowMode))
|
||||
{ renderer.shadowCastingMode = shadowMode; changes.Add("shadowCastingMode"); }
|
||||
if (@params["receiveShadows"] != null) { renderer.receiveShadows = @params["receiveShadows"].ToObject<bool>(); 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<UnityEngine.Rendering.LightProbeUsage>(@params["lightProbeUsage"].ToString(), true, out var probeUsage))
|
||||
{ renderer.lightProbeUsage = probeUsage; changes.Add("lightProbeUsage"); }
|
||||
if (@params["reflectionProbeUsage"] != null && Enum.TryParse<UnityEngine.Rendering.ReflectionProbeUsage>(@params["reflectionProbeUsage"].ToString(), true, out var reflectionUsage))
|
||||
{ renderer.reflectionProbeUsage = reflectionUsage; changes.Add("reflectionProbeUsage"); }
|
||||
|
||||
// Motion vectors
|
||||
if (@params["motionVectorGenerationMode"] != null && Enum.TryParse<MotionVectorGenerationMode>(@params["motionVectorGenerationMode"].ToString(), true, out var motionMode))
|
||||
{ renderer.motionVectorGenerationMode = motionMode; changes.Add("motionVectorGenerationMode"); }
|
||||
|
||||
// Sorting
|
||||
if (@params["sortingOrder"] != null) { renderer.sortingOrder = @params["sortingOrder"].ToObject<int>(); changes.Add("sortingOrder"); }
|
||||
if (@params["sortingLayerName"] != null) { renderer.sortingLayerName = @params["sortingLayerName"].ToString(); changes.Add("sortingLayerName"); }
|
||||
if (@params["sortingLayerID"] != null) { renderer.sortingLayerID = @params["sortingLayerID"].ToObject<int>(); changes.Add("sortingLayerID"); }
|
||||
|
||||
// Rendering layer mask (for SRP)
|
||||
if (@params["renderingLayerMask"] != null) { renderer.renderingLayerMask = @params["renderingLayerMask"].ToObject<uint>(); changes.Add("renderingLayerMask"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets common Renderer properties for GetInfo methods.
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets width properties for LineRenderer or TrailRenderer.
|
||||
/// </summary>
|
||||
/// <param name="params">JSON parameters containing width, startWidth, endWidth, widthCurve, widthMultiplier</param>
|
||||
/// <param name="changes">List to track changed properties</param>
|
||||
/// <param name="setStartWidth">Action to set start width</param>
|
||||
/// <param name="setEndWidth">Action to set end width</param>
|
||||
/// <param name="setWidthCurve">Action to set width curve</param>
|
||||
/// <param name="setWidthMultiplier">Action to set width multiplier</param>
|
||||
/// <param name="parseAnimationCurve">Function to parse animation curve from JToken</param>
|
||||
public static void ApplyWidthProperties(JObject @params, List<string> changes,
|
||||
Action<float> setStartWidth, Action<float> setEndWidth,
|
||||
Action<AnimationCurve> setWidthCurve, Action<float> setWidthMultiplier,
|
||||
Func<JToken, float, AnimationCurve> parseAnimationCurve)
|
||||
{
|
||||
if (@params["width"] != null)
|
||||
{
|
||||
float w = @params["width"].ToObject<float>();
|
||||
setStartWidth(w);
|
||||
setEndWidth(w);
|
||||
changes.Add("width");
|
||||
}
|
||||
if (@params["startWidth"] != null) { setStartWidth(@params["startWidth"].ToObject<float>()); changes.Add("startWidth"); }
|
||||
if (@params["endWidth"] != null) { setEndWidth(@params["endWidth"].ToObject<float>()); changes.Add("endWidth"); }
|
||||
if (@params["widthCurve"] != null) { setWidthCurve(parseAnimationCurve(@params["widthCurve"], 1f)); changes.Add("widthCurve"); }
|
||||
if (@params["widthMultiplier"] != null) { setWidthMultiplier(@params["widthMultiplier"].ToObject<float>()); changes.Add("widthMultiplier"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets color properties for LineRenderer or TrailRenderer.
|
||||
/// </summary>
|
||||
/// <param name="params">JSON parameters containing color, startColor, endColor, gradient</param>
|
||||
/// <param name="changes">List to track changed properties</param>
|
||||
/// <param name="setStartColor">Action to set start color</param>
|
||||
/// <param name="setEndColor">Action to set end color</param>
|
||||
/// <param name="setGradient">Action to set gradient</param>
|
||||
/// <param name="parseColor">Function to parse color from JToken</param>
|
||||
/// <param name="parseGradient">Function to parse gradient from JToken</param>
|
||||
/// <param name="fadeEndAlpha">If true, sets end color alpha to 0 when using single color</param>
|
||||
public static void ApplyColorProperties(JObject @params, List<string> changes,
|
||||
Action<Color> setStartColor, Action<Color> setEndColor,
|
||||
Action<Gradient> setGradient,
|
||||
Func<JToken, Color> parseColor, Func<JToken, Gradient> 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"); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets material for a Renderer.
|
||||
/// </summary>
|
||||
/// <param name="renderer">The renderer to set material on</param>
|
||||
/// <param name="params">JSON parameters containing materialPath</param>
|
||||
/// <param name="undoName">Name for the undo operation</param>
|
||||
/// <param name="findMaterial">Function to find material by path</param>
|
||||
public static object SetRendererMaterial(Renderer renderer, JObject @params, string undoName, Func<string, Material> 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}" };
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Applies Line/Trail specific properties (loop, alignment, textureMode, etc.).
|
||||
/// </summary>
|
||||
public static void ApplyLineTrailProperties(JObject @params, List<string> changes,
|
||||
Action<bool> setLoop, Action<bool> setUseWorldSpace,
|
||||
Action<int> setNumCornerVertices, Action<int> setNumCapVertices,
|
||||
Action<LineAlignment> setAlignment, Action<LineTextureMode> setTextureMode,
|
||||
Action<bool> setGenerateLightingData)
|
||||
{
|
||||
if (@params["loop"] != null && setLoop != null) { setLoop(@params["loop"].ToObject<bool>()); changes.Add("loop"); }
|
||||
if (@params["useWorldSpace"] != null && setUseWorldSpace != null) { setUseWorldSpace(@params["useWorldSpace"].ToObject<bool>()); changes.Add("useWorldSpace"); }
|
||||
if (@params["numCornerVertices"] != null && setNumCornerVertices != null) { setNumCornerVertices(@params["numCornerVertices"].ToObject<int>()); changes.Add("numCornerVertices"); }
|
||||
if (@params["numCapVertices"] != null && setNumCapVertices != null) { setNumCapVertices(@params["numCapVertices"].ToObject<int>()); changes.Add("numCapVertices"); }
|
||||
if (@params["alignment"] != null && setAlignment != null && Enum.TryParse<LineAlignment>(@params["alignment"].ToString(), true, out var align)) { setAlignment(align); changes.Add("alignment"); }
|
||||
if (@params["textureMode"] != null && setTextureMode != null && Enum.TryParse<LineTextureMode>(@params["textureMode"].ToString(), true, out var texMode)) { setTextureMode(texMode); changes.Add("textureMode"); }
|
||||
if (@params["generateLightingData"] != null && setGenerateLightingData != null) { setGenerateLightingData(@params["generateLightingData"].ToObject<bool>()); changes.Add("generateLightingData"); }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f3a7e2d5c1b4a9e6d0f8c3b2a1e5d7c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 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}.
|
||||
/// </summary>
|
||||
public static class VectorParsing
|
||||
|
|
@ -224,6 +225,242 @@ namespace MCPForUnity.Editor.Helpers
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a JToken into a Color, returning a default value if parsing fails.
|
||||
/// Added for ManageVFX refactoring.
|
||||
/// </summary>
|
||||
public static Color ParseColorOrDefault(JToken token, Color defaultValue = default)
|
||||
{
|
||||
if (defaultValue == default) defaultValue = Color.black;
|
||||
return ParseColor(token) ?? defaultValue;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parses a JToken (array or object) into a Vector4.
|
||||
/// Added for ManageVFX refactoring.
|
||||
/// </summary>
|
||||
/// <param name="token">The JSON token to parse</param>
|
||||
/// <returns>The parsed Vector4 or null if parsing fails</returns>
|
||||
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<float>(),
|
||||
array[1].ToObject<float>(),
|
||||
array[2].ToObject<float>(),
|
||||
array[3].ToObject<float>()
|
||||
);
|
||||
}
|
||||
|
||||
// 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<float>(),
|
||||
obj["y"].ToObject<float>(),
|
||||
obj["z"].ToObject<float>(),
|
||||
obj["w"].ToObject<float>()
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[VectorParsing] Failed to parse Vector4 from '{token}': {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a JToken into a Vector4, returning a default value if parsing fails.
|
||||
/// Added for ManageVFX refactoring.
|
||||
/// </summary>
|
||||
public static Vector4 ParseVector4OrDefault(JToken token, Vector4 defaultValue = default)
|
||||
{
|
||||
return ParseVector4(token) ?? defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="token">The JSON token to parse</param>
|
||||
/// <returns>The parsed Gradient or null if parsing fails</returns>
|
||||
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<float>() ?? startColor.a;
|
||||
float endAlpha = obj["endAlpha"]?.ToObject<float>() ?? 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<GradientColorKey>();
|
||||
var alphaKeys = new List<GradientAlphaKey>();
|
||||
|
||||
if (obj["colorKeys"] is JArray colorKeysArr)
|
||||
{
|
||||
foreach (var key in colorKeysArr)
|
||||
{
|
||||
Color color = ParseColorOrDefault(key["color"]);
|
||||
float time = key["time"]?.ToObject<float>() ?? 0f;
|
||||
colorKeys.Add(new GradientColorKey(color, time));
|
||||
}
|
||||
}
|
||||
|
||||
if (obj["alphaKeys"] is JArray alphaKeysArr)
|
||||
{
|
||||
foreach (var key in alphaKeysArr)
|
||||
{
|
||||
float alpha = key["alpha"]?.ToObject<float>() ?? 1f;
|
||||
float time = key["time"]?.ToObject<float>() ?? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a JToken into a Gradient, returning a default gradient if parsing fails.
|
||||
/// Added for ManageVFX refactoring.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="token">The JSON token to parse</param>
|
||||
/// <returns>The parsed AnimationCurve or null if parsing fails</returns>
|
||||
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<float>());
|
||||
}
|
||||
|
||||
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<float>() ?? 0f;
|
||||
float value = key["value"]?.ToObject<float>() ?? 1f;
|
||||
float inTangent = key["inTangent"]?.ToObject<float>() ?? 0f;
|
||||
float outTangent = key["outTangent"]?.ToObject<float>() ?? 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<float>() ?? obj["startValue"]?.ToObject<float>() ?? 1f;
|
||||
float endValue = obj["end"]?.ToObject<float>() ?? obj["endValue"]?.ToObject<float>() ?? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a JToken into an AnimationCurve, returning a constant curve if parsing fails.
|
||||
/// Added for ManageVFX refactoring.
|
||||
/// </summary>
|
||||
/// <param name="token">The JSON token to parse</param>
|
||||
/// <param name="defaultValue">The constant value for the default curve</param>
|
||||
public static AnimationCurve ParseAnimationCurveOrDefault(JToken token, float defaultValue = 1f)
|
||||
{
|
||||
return ParseAnimationCurve(token) ?? AnimationCurve.Constant(0f, 1f, defaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a JToken into a Rect.
|
||||
/// Supports {x, y, width, height} format.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a8f3d2c1e9b74f6a8c5d0e2f1a3b4c5d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
|
||||
|
|
@ -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<T>)
|
||||
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)}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue