using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using MCPForUnity.Runtime.Helpers; namespace MCPForUnity.Editor.Tools.Graphics { internal static class VolumeOps { // === volume_create === // Params: name (string), is_global (bool, default true), weight (float, default 1), // priority (float, default 0), profile_path (string, optional - path to save VolumeProfile asset), // effects (array of {type, ...params}, optional - effects to add immediately) internal static object CreateVolume(JObject @params) { var p = new ToolParams(@params); string name = p.Get("name") ?? "Volume"; bool isGlobal = p.GetBool("is_global", true); float weight = p.GetFloat("weight") ?? 1.0f; float priority = p.GetFloat("priority") ?? 0f; string profilePath = p.Get("profile_path"); if (!string.IsNullOrEmpty(profilePath)) { if (!profilePath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) && !profilePath.StartsWith("Assets\\", StringComparison.OrdinalIgnoreCase)) profilePath = "Assets/" + profilePath; if (!profilePath.EndsWith(".asset", StringComparison.OrdinalIgnoreCase)) profilePath += ".asset"; } var go = new GameObject(name); Undo.RegisterCreatedObjectUndo(go, $"Create Volume '{name}'"); // Add Volume component via reflection var volumeComp = go.AddComponent(GraphicsHelpers.VolumeType); // Set properties via reflection SetProperty(volumeComp, "isGlobal", isGlobal); SetProperty(volumeComp, "weight", weight); SetProperty(volumeComp, "priority", priority); // Create or load VolumeProfile object profile; if (!string.IsNullOrEmpty(profilePath)) { // Load existing or create new profile asset profile = AssetDatabase.LoadAssetAtPath(profilePath, GraphicsHelpers.VolumeProfileType); if (profile == null) { profile = ScriptableObject.CreateInstance(GraphicsHelpers.VolumeProfileType); // Ensure directory exists var dir = System.IO.Path.GetDirectoryName(profilePath); if (!string.IsNullOrEmpty(dir) && !System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir); AssetDatabase.CreateAsset((UnityEngine.Object)profile, profilePath); } } else { // Create embedded profile (not saved as asset) profile = ScriptableObject.CreateInstance(GraphicsHelpers.VolumeProfileType); } // Assign profile (sharedProfile is a public field, handled by SetProperty's field fallback) SetProperty(volumeComp, "sharedProfile", profile); // Add initial effects if provided var effectsToken = p.GetRaw("effects") as JArray; var addedEffects = new List(); if (effectsToken != null) { foreach (var effectDef in effectsToken) { if (effectDef is JObject effectObj) { string effectType = ParamCoercion.CoerceString(effectObj["type"], null); if (string.IsNullOrEmpty(effectType)) continue; var type = GraphicsHelpers.ResolveVolumeComponentType(effectType); if (type == null) continue; // profile.Add(type, true) var addMethod = GraphicsHelpers.VolumeProfileType.GetMethod("Add", new[] { typeof(Type), typeof(bool) }); if (addMethod != null) { var component = addMethod.Invoke(profile, new object[] { type, true }); if (component != null) { // Set parameters — support both nested {"parameters": {...}} and flat fields var paramObj = effectObj["parameters"] as JObject; if (paramObj != null) { foreach (var pp in paramObj.Properties()) SetVolumeParameter(component, pp.Name, pp.Value); } else { foreach (var prop in effectObj.Properties()) { if (prop.Name == "type") continue; SetVolumeParameter(component, prop.Name, prop.Value); } } addedEffects.Add(effectType); } } } } if (profile is UnityEngine.Object profileObj) EditorUtility.SetDirty(profileObj); } GraphicsHelpers.MarkDirty(volumeComp); return new { success = true, message = $"Created {(isGlobal ? "global" : "local")} Volume '{name}'" + (addedEffects.Count > 0 ? $" with effects: {string.Join(", ", addedEffects)}" : ""), data = new { instanceID = go.GetInstanceIDCompat(), isGlobal, weight, priority, profilePath = profilePath ?? "(embedded)", effects = addedEffects } }; } // === volume_add_effect === // Params: target (string/int), effect (string - type name like "Bloom") internal static object AddEffect(JObject @params) { var p = new ToolParams(@params); string effectName = p.Get("effect"); if (string.IsNullOrEmpty(effectName)) return new ErrorResponse("'effect' parameter is required (e.g., 'Bloom', 'Vignette')."); var volume = GraphicsHelpers.FindVolume(@params); if (volume == null) return new ErrorResponse("Volume not found. Specify 'target' (name or instance ID)."); var effectType = GraphicsHelpers.ResolveVolumeComponentType(effectName); if (effectType == null) { var available = GraphicsHelpers.GetAvailableEffectTypes() .Select(t => t.Name).ToList(); return new ErrorResponse( $"Effect type '{effectName}' not found. Available: {string.Join(", ", available.Take(20))}"); } var profile = GetProperty(volume, "sharedProfile"); if (profile == null) return new ErrorResponse("Volume has no profile assigned."); // Check if effect already exists var components = GetProperty(profile, "components") as System.Collections.IList; if (components != null) { foreach (var comp in components) { if (comp != null && comp.GetType() == effectType) return new ErrorResponse($"Effect '{effectName}' already exists on this Volume. Use volume_set_effect to modify it."); } } // profile.Add(effectType, true) -- 'true' means override all params var addMethod = GraphicsHelpers.VolumeProfileType.GetMethod("Add", new[] { typeof(Type), typeof(bool) }); if (addMethod == null) return new ErrorResponse("Could not find VolumeProfile.Add method."); var component = addMethod.Invoke(profile, new object[] { effectType, true }); if (component == null) return new ErrorResponse($"Failed to add effect '{effectName}'."); if (profile is UnityEngine.Object profileObj) EditorUtility.SetDirty(profileObj); GraphicsHelpers.MarkDirty(volume); return new { success = true, message = $"Added '{effectName}' to Volume '{(volume as Component)?.gameObject.name}'.", data = new { effect = effectName, volumeInstanceID = (volume as Component)?.gameObject.GetInstanceIDCompat() } }; } // === volume_set_effect === // Params: target (string/int), effect (string), parameters (dict of field->value) internal static object SetEffect(JObject @params) { var p = new ToolParams(@params); string effectName = p.Get("effect"); if (string.IsNullOrEmpty(effectName)) return new ErrorResponse("'effect' parameter is required."); var volume = GraphicsHelpers.FindVolume(@params); if (volume == null) return new ErrorResponse("Volume not found. Specify 'target'."); var profile = GetProperty(volume, "sharedProfile"); if (profile == null) return new ErrorResponse("Volume has no profile assigned."); var effectType = GraphicsHelpers.ResolveVolumeComponentType(effectName); if (effectType == null) return new ErrorResponse($"Effect type '{effectName}' not found."); // Find the effect component in the profile var components = GetProperty(profile, "components") as System.Collections.IList; if (components == null) return new ErrorResponse("Could not read profile components."); object targetComponent = null; foreach (var comp in components) { if (comp != null && comp.GetType() == effectType) { targetComponent = comp; break; } } if (targetComponent == null) return new ErrorResponse($"Effect '{effectName}' not found on this Volume. Use volume_add_effect first."); // Set parameters var parameters = p.GetRaw("parameters") as JObject; if (parameters == null) return new ErrorResponse("'parameters' dict is required."); var setParams = new List(); var failedParams = new List(); foreach (var prop in parameters.Properties()) { if (SetVolumeParameter(targetComponent, prop.Name, prop.Value)) setParams.Add(prop.Name); else failedParams.Add(prop.Name); } if (profile is UnityEngine.Object profileObj) EditorUtility.SetDirty(profileObj); var msg = $"Set {setParams.Count} parameter(s) on '{effectName}'"; if (failedParams.Count > 0) msg += $". Failed: {string.Join(", ", failedParams)}"; return new { success = true, message = msg, data = new { effect = effectName, set = setParams, failed = failedParams } }; } // === volume_remove_effect === // Params: target, effect internal static object RemoveEffect(JObject @params) { var p = new ToolParams(@params); string effectName = p.Get("effect"); if (string.IsNullOrEmpty(effectName)) return new ErrorResponse("'effect' parameter is required."); var volume = GraphicsHelpers.FindVolume(@params); if (volume == null) return new ErrorResponse("Volume not found."); var effectType = GraphicsHelpers.ResolveVolumeComponentType(effectName); if (effectType == null) return new ErrorResponse($"Effect type '{effectName}' not found."); var profile = GetProperty(volume, "sharedProfile"); if (profile == null) return new ErrorResponse("Volume has no profile."); // Check if effect exists before removing bool found = false; var components = GetProperty(profile, "components") as System.Collections.IList; if (components != null) { foreach (var comp in components) { if (comp != null && comp.GetType() == effectType) { found = true; break; } } } if (!found) return new ErrorResponse($"Effect '{effectName}' not found on this Volume."); var removeMethod = GraphicsHelpers.VolumeProfileType.GetMethod("Remove", new[] { typeof(Type) }); if (removeMethod == null) return new ErrorResponse("Could not find VolumeProfile.Remove method."); removeMethod.Invoke(profile, new object[] { effectType }); if (profile is UnityEngine.Object profileObj) EditorUtility.SetDirty(profileObj); GraphicsHelpers.MarkDirty(volume); return new { success = true, message = $"Removed '{effectName}' from Volume.", data = new { effect = effectName } }; } // === volume_get_info === // Params: target (optional -- if omitted, returns info for all volumes) internal static object GetInfo(JObject @params) { var volume = GraphicsHelpers.FindVolume(@params); if (volume == null) return new ErrorResponse("Volume not found."); var info = BuildVolumeInfo(volume); return new { success = true, message = $"Volume info for '{(volume as Component)?.gameObject.name}'.", data = info }; } // === volume_set_properties === // Params: target, weight, priority, is_global, blend_distance // OR properties dict with those keys internal static object SetProperties(JObject @params) { var p = new ToolParams(@params); var volume = GraphicsHelpers.FindVolume(@params); if (volume == null) return new ErrorResponse("Volume not found."); // Unpack "properties" dict into top-level params so callers can use either style var propsDict = p.GetRaw("properties") as JObject; if (propsDict != null) { foreach (var prop in propsDict.Properties()) { if (@params[prop.Name] == null) @params[prop.Name] = prop.Value; } p = new ToolParams(@params); } var changed = new List(); var weight = p.GetFloat("weight"); if (weight.HasValue) { SetProperty(volume, "weight", weight.Value); changed.Add("weight"); } var priority = p.GetFloat("priority"); if (priority.HasValue) { SetProperty(volume, "priority", priority.Value); changed.Add("priority"); } if (p.Has("is_global")) { SetProperty(volume, "isGlobal", p.GetBool("is_global")); changed.Add("isGlobal"); } var blendDist = p.GetFloat("blend_distance"); if (blendDist.HasValue) { SetProperty(volume, "blendDistance", blendDist.Value); changed.Add("blendDistance"); } if (changed.Count == 0) return new ErrorResponse("No properties specified. Use: weight, priority, is_global, blend_distance."); GraphicsHelpers.MarkDirty(volume); return new { success = true, message = $"Updated Volume properties: {string.Join(", ", changed)}", data = new { changed } }; } // === volume_list_effects === // No params needed -- lists all available VolumeComponent types internal static object ListEffects(JObject @params) { var types = GraphicsHelpers.GetAvailableEffectTypes(); var effectList = types.Select(t => new { name = t.Name, fullName = t.FullName, ns = t.Namespace }).ToList(); return new { success = true, message = $"Found {effectList.Count} available volume effects.", data = new { pipeline = GraphicsHelpers.GetPipelineName(), effects = effectList } }; } // === volume_create_profile === // Params: path (string -- asset path like "Assets/Settings/MyProfile.asset") internal static object CreateProfile(JObject @params) { var p = new ToolParams(@params); string path = p.Get("path"); if (string.IsNullOrEmpty(path)) return new ErrorResponse("'path' parameter is required (e.g., 'Settings/MyProfile' or 'Assets/Settings/MyProfile.asset')."); // Auto-prepend Assets/ if missing (paths are relative to Assets/ by convention) if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) && !path.StartsWith("Assets\\", StringComparison.OrdinalIgnoreCase)) path = "Assets/" + path; if (!path.EndsWith(".asset", StringComparison.OrdinalIgnoreCase)) path += ".asset"; // Ensure directory exists var dir = System.IO.Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !AssetDatabase.IsValidFolder(dir)) { // Create folders recursively var parts = dir.Replace("\\", "/").Split('/'); string current = parts[0]; for (int i = 1; i < parts.Length; i++) { string next = current + "/" + parts[i]; if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(current, parts[i]); current = next; } } var profile = ScriptableObject.CreateInstance(GraphicsHelpers.VolumeProfileType); AssetDatabase.CreateAsset(profile, path); AssetDatabase.SaveAssets(); return new { success = true, message = $"Created VolumeProfile at '{path}'.", data = new { path } }; } // === ListVolumes (used by VolumesResource) === internal static object ListVolumes(JObject @params) { if (!GraphicsHelpers.HasVolumeSystem) return new { success = true, message = "Volume system not available.", data = new { volumes = new List() } }; var allVolumes = UnityFindObjectsCompat.FindAll(GraphicsHelpers.VolumeType); var volumeList = new List(); foreach (Component vol in allVolumes) { volumeList.Add(BuildVolumeInfo(vol)); } return new { success = true, message = $"Found {volumeList.Count} volume(s).", data = new { pipeline = GraphicsHelpers.GetPipelineName(), volumes = volumeList } }; } // --- Helper: Build info object for a single Volume --- private static object BuildVolumeInfo(object volumeComponent) { var comp = volumeComponent as Component; if (comp == null) return null; bool isGlobal = GetPropertyValue(volumeComponent, "isGlobal", true); float weight = GetPropertyValue(volumeComponent, "weight", 1f); float priority = GetPropertyValue(volumeComponent, "priority", 0f); float blendDistance = GetPropertyValue(volumeComponent, "blendDistance", 0f); var profile = GetProperty(volumeComponent, "sharedProfile"); string profileName = profile is UnityEngine.Object profileObj2 ? profileObj2.name : null; string profilePath = profile is UnityEngine.Object po ? AssetDatabase.GetAssetPath(po) : null; var effectsList = new List(); if (profile != null) { var components = GetProperty(profile, "components") as System.Collections.IList; if (components != null) { foreach (var effect in components) { if (effect == null) continue; var effectType = effect.GetType(); bool active = GetPropertyValue(effect, "active", true); // Collect overridden parameters var overriddenParams = new List(); foreach (var field in effectType.GetFields(BindingFlags.Public | BindingFlags.Instance)) { var fieldValue = field.GetValue(effect); if (fieldValue == null) continue; var overrideProp = fieldValue.GetType().GetProperty("overrideState"); if (overrideProp != null) { bool overridden = (bool)overrideProp.GetValue(fieldValue); if (overridden) overriddenParams.Add(field.Name); } } effectsList.Add(new { type = effectType.Name, active, overridden_params = overriddenParams }); } } } return new { name = comp.gameObject.name, instance_id = comp.gameObject.GetInstanceIDCompat(), is_global = isGlobal, weight, priority, blend_distance = blendDistance, profile = profileName, profile_path = profilePath ?? "", effects = effectsList }; } // --- Helper: Set a VolumeParameter field value via reflection --- // VolumeParameter has: overrideState (bool), value (T) internal static bool SetVolumeParameter(object component, string fieldName, JToken value) { if (component == null || string.IsNullOrEmpty(fieldName)) return false; var field = component.GetType().GetField(fieldName, BindingFlags.Public | BindingFlags.Instance); if (field == null) { // Try camelCase conversion from snake_case string camelCase = StringCaseUtility.ToCamelCase(fieldName); field = component.GetType().GetField(camelCase, BindingFlags.Public | BindingFlags.Instance); } if (field == null) return false; var param = field.GetValue(component); if (param == null) return false; // Set value with type conversion, then enable override on success var valueProp = param.GetType().GetProperty("value"); if (valueProp == null) return false; try { object converted = ConvertToParameterType(value, valueProp.PropertyType); valueProp.SetValue(param, converted); var overrideProp = param.GetType().GetProperty("overrideState"); if (overrideProp != null) overrideProp.SetValue(param, true); return true; } catch (Exception ex) { McpLog.Warn($"[VolumeOps] Failed to set '{fieldName}': {ex.Message}"); return false; } } // --- Helper: Convert JToken to target parameter type --- private static object ConvertToParameterType(JToken value, Type targetType) { if (value == null || value.Type == JTokenType.Null) return null; // Handle Color if (targetType == typeof(Color)) { if (value is JArray arr && arr.Count >= 3) { float r = arr[0].Value(); float g = arr[1].Value(); float b = arr[2].Value(); float a = arr.Count >= 4 ? arr[3].Value() : 1f; return new Color(r, g, b, a); } // Try hex string if (value.Type == JTokenType.String) { if (ColorUtility.TryParseHtmlString(value.ToString(), out Color c)) return c; } } // Handle Vector2 if (targetType == typeof(Vector2)) { if (value is JArray arr && arr.Count >= 2) return new Vector2(arr[0].Value(), arr[1].Value()); } // Handle Vector3 if (targetType == typeof(Vector3)) { if (value is JArray arr && arr.Count >= 3) return new Vector3(arr[0].Value(), arr[1].Value(), arr[2].Value()); } // Handle Vector4 if (targetType == typeof(Vector4)) { if (value is JArray arr && arr.Count >= 4) return new Vector4(arr[0].Value(), arr[1].Value(), arr[2].Value(), arr[3].Value()); } // Handle enums if (targetType.IsEnum) { string str = value.ToString(); if (Enum.TryParse(targetType, str, true, out object enumVal)) return enumVal; // Try as int if (int.TryParse(str, out int intVal)) return Enum.ToObject(targetType, intVal); } // Handle bool if (targetType == typeof(bool)) return ParamCoercion.CoerceBool(value, false); // Handle float if (targetType == typeof(float)) return ParamCoercion.CoerceFloat(value, 0f); // Handle int if (targetType == typeof(int)) return ParamCoercion.CoerceInt(value, 0); // Handle Texture2D (by asset path) if (targetType == typeof(Texture2D) || targetType == typeof(Texture)) { string path = value.ToString(); return AssetDatabase.LoadAssetAtPath(path); } // Fallback: try Convert try { return Convert.ChangeType(value.ToObject(), targetType); } catch { return value.ToObject(); } } // --- Reflection helpers (with field fallback for Volume.sharedProfile etc.) --- private static object GetProperty(object obj, string name) { if (obj == null) return null; var prop = obj.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance); if (prop != null) return prop.GetValue(obj); // Fallback: try as a field (e.g., Volume.sharedProfile is a public field, not a property) var field = obj.GetType().GetField(name, BindingFlags.Public | BindingFlags.Instance); return field?.GetValue(obj); } private static T GetPropertyValue(object obj, string name, T defaultValue) { var val = GetProperty(obj, name); if (val is T typed) return typed; return defaultValue; } private static void SetProperty(object obj, string name, object value) { if (obj == null) return; var prop = obj.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance); if (prop != null && prop.CanWrite) { prop.SetValue(obj, value); return; } // Fallback: try as a field (e.g., Volume.sharedProfile is a public field, not a property) var field = obj.GetType().GetField(name, BindingFlags.Public | BindingFlags.Instance); field?.SetValue(obj, value); } } }