using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Tools.Physics { internal static class PhysicsMaterialOps { public static object Create(JObject @params) { var p = new ToolParams(@params); string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant(); var nameResult = p.GetRequired("name"); var nameErr = nameResult.GetOrError(out string name); if (nameErr != null) return nameErr; string folder = p.Get("path") ?? "Assets/Physics Materials"; folder = AssetPathUtility.SanitizeAssetPath(folder); if (string.IsNullOrEmpty(folder)) return new ErrorResponse("Invalid folder path."); if (!EnsureFolderExists(folder, out string folderError)) return new ErrorResponse(folderError); if (dimension == "2d") return Create2D(name, folder, p); if (dimension != "3d") return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'."); return Create3D(name, folder, p); } public static object Configure(JObject @params) { var p = new ToolParams(@params); string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant(); var pathResult = p.GetRequired("path"); var pathErr = pathResult.GetOrError(out string path); if (pathErr != null) return pathErr; path = AssetPathUtility.SanitizeAssetPath(path); if (string.IsNullOrEmpty(path)) return new ErrorResponse("Invalid asset path."); var properties = p.GetRaw("properties") as JObject; if (properties == null || properties.Count == 0) return new ErrorResponse("'properties' parameter is required and must be a non-empty object."); if (dimension != "3d" && dimension != "2d") return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'."); if (dimension == "2d") return Configure2D(path, properties); return Configure3D(path, properties); } public static object Assign(JObject @params) { var p = new ToolParams(@params); var targetToken = p.GetRaw("target"); if (targetToken == null || string.IsNullOrEmpty(targetToken.ToString())) return new ErrorResponse("'target' parameter is required."); var matPathResult = p.GetRequired("material_path"); var matPathErr = matPathResult.GetOrError(out string materialPath); if (matPathErr != null) return matPathErr; materialPath = AssetPathUtility.SanitizeAssetPath(materialPath); if (string.IsNullOrEmpty(materialPath)) return new ErrorResponse("Invalid material path."); string searchMethod = p.Get("search_method") ?? "by_name"; string colliderType = p.Get("collider_type"); int? componentIndex = ParamCoercion.CoerceIntNullable(p.GetRaw("componentIndex") ?? p.GetRaw("component_index")); var go = GameObjectLookup.FindByTarget(targetToken, searchMethod); if (go == null) return new ErrorResponse($"GameObject not found: '{targetToken}'."); // Try to load as 3D physics material first #if UNITY_6000_0_OR_NEWER var mat3D = AssetDatabase.LoadAssetAtPath(materialPath); #else var mat3D = AssetDatabase.LoadAssetAtPath(materialPath); #endif var mat2D = AssetDatabase.LoadAssetAtPath(materialPath); if (mat3D == null && mat2D == null) return new ErrorResponse($"No physics material found at path: '{materialPath}'."); // Try 3D colliders first if (mat3D != null) { var collider3D = FindCollider3D(go, colliderType, componentIndex); if (collider3D != null) { Undo.RecordObject(collider3D, "Assign Physics Material"); collider3D.sharedMaterial = mat3D; EditorUtility.SetDirty(collider3D); return new { success = true, message = $"Assigned 3D physics material to {collider3D.GetType().Name} on '{go.name}'.", data = new { gameObject = go.name, collider = collider3D.GetType().Name, materialPath } }; } if (componentIndex.HasValue) { var type3D = !string.IsNullOrEmpty(colliderType) ? UnityTypeResolver.ResolveComponent(colliderType) : typeof(Collider); if (type3D != null && typeof(Collider).IsAssignableFrom(type3D)) { int count3D = go.GetComponents(type3D).Length; return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {count3D} '{type3D.Name}' collider(s) on '{go.name}'."); } else if (!string.IsNullOrEmpty(colliderType)) { return new ErrorResponse($"Unknown or invalid 3D collider type: '{colliderType}'."); } } } // Try 2D colliders if (mat2D != null) { var collider2D = FindCollider2D(go, colliderType, componentIndex); if (collider2D != null) { Undo.RecordObject(collider2D, "Assign Physics Material 2D"); collider2D.sharedMaterial = mat2D; EditorUtility.SetDirty(collider2D); return new { success = true, message = $"Assigned 2D physics material to {collider2D.GetType().Name} on '{go.name}'.", data = new { gameObject = go.name, collider = collider2D.GetType().Name, materialPath } }; } if (componentIndex.HasValue) { var type2D = !string.IsNullOrEmpty(colliderType) ? UnityTypeResolver.ResolveComponent(colliderType) : typeof(Collider2D); if (type2D != null && typeof(Collider2D).IsAssignableFrom(type2D)) { int count2D = go.GetComponents(type2D).Length; return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {count2D} '{type2D.Name}' collider(s) on '{go.name}'."); } else if (!string.IsNullOrEmpty(colliderType)) { return new ErrorResponse($"Unknown or invalid 2D collider type: '{colliderType}'."); } } } return new ErrorResponse($"No suitable collider found on '{go.name}'."); } // ===================================================================== // Create helpers // ===================================================================== private static object Create3D(string name, string folder, ToolParams p) { float dynamicFriction = p.GetFloat("dynamic_friction") ?? 0.6f; float staticFriction = p.GetFloat("static_friction") ?? 0.6f; float bounciness = p.GetFloat("bounciness") ?? 0f; string frictionCombine = p.Get("friction_combine"); string bounceCombine = p.Get("bounce_combine"); string assetPath = $"{folder}/{name}.physicMaterial"; if (AssetDatabase.LoadAssetAtPath(assetPath) != null) return new ErrorResponse($"A physics material already exists at '{assetPath}'. Use configure_physics_material to modify it."); #if UNITY_6000_0_OR_NEWER var mat = new PhysicsMaterial(name) { dynamicFriction = dynamicFriction, staticFriction = staticFriction, bounciness = bounciness }; if (!string.IsNullOrEmpty(frictionCombine) && Enum.TryParse(frictionCombine, true, out var fc)) mat.frictionCombine = fc; if (!string.IsNullOrEmpty(bounceCombine) && Enum.TryParse(bounceCombine, true, out var bc)) mat.bounceCombine = bc; #else var mat = new PhysicMaterial(name) { dynamicFriction = dynamicFriction, staticFriction = staticFriction, bounciness = bounciness }; if (!string.IsNullOrEmpty(frictionCombine) && Enum.TryParse(frictionCombine, true, out var fc)) mat.frictionCombine = fc; if (!string.IsNullOrEmpty(bounceCombine) && Enum.TryParse(bounceCombine, true, out var bc)) mat.bounceCombine = bc; #endif AssetDatabase.CreateAsset(mat, assetPath); AssetDatabase.SaveAssets(); return new { success = true, message = $"Created 3D physics material '{name}' at '{assetPath}'.", data = new { path = assetPath, dimension = "3d", dynamicFriction, staticFriction, bounciness, frictionCombine = mat.frictionCombine.ToString(), bounceCombine = mat.bounceCombine.ToString() } }; } private static object Create2D(string name, string folder, ToolParams p) { float friction = p.GetFloat("friction") ?? 0.4f; float bounciness = p.GetFloat("bounciness") ?? 0f; string assetPath = $"{folder}/{name}.physicsMaterial2D"; if (AssetDatabase.LoadAssetAtPath(assetPath) != null) return new ErrorResponse($"A 2D physics material already exists at '{assetPath}'. Use configure_physics_material to modify it."); var mat = new PhysicsMaterial2D(name) { friction = friction, bounciness = bounciness }; AssetDatabase.CreateAsset(mat, assetPath); AssetDatabase.SaveAssets(); return new { success = true, message = $"Created 2D physics material '{name}' at '{assetPath}'.", data = new { path = assetPath, dimension = "2d", friction, bounciness } }; } // ===================================================================== // Configure helpers // ===================================================================== private static readonly HashSet Valid3DMatKeys = new HashSet { "dynamicfriction", "staticfriction", "bounciness", "frictioncombine", "bouncecombine" }; private static object Configure3D(string path, JObject properties) { // Validate all keys before applying any changes var unknown = new List(); foreach (var prop in properties.Properties()) { string key = prop.Name.ToLowerInvariant().Replace("_", ""); if (!Valid3DMatKeys.Contains(key)) unknown.Add(prop.Name); } if (unknown.Count > 0) return new ErrorResponse( $"Unknown 3D physics material property(ies): {string.Join(", ", unknown)}."); #if UNITY_6000_0_OR_NEWER var mat = AssetDatabase.LoadAssetAtPath(path); #else var mat = AssetDatabase.LoadAssetAtPath(path); #endif if (mat == null) return new ErrorResponse($"No 3D physics material found at: '{path}'."); Undo.RecordObject(mat, "Configure Physics Material"); var changed = new List(); foreach (var prop in properties.Properties()) { string key = prop.Name.ToLowerInvariant().Replace("_", ""); switch (key) { case "dynamicfriction": mat.dynamicFriction = prop.Value.Value(); changed.Add("dynamicFriction"); break; case "staticfriction": mat.staticFriction = prop.Value.Value(); changed.Add("staticFriction"); break; case "bounciness": mat.bounciness = prop.Value.Value(); changed.Add("bounciness"); break; case "frictioncombine": { #if UNITY_6000_0_OR_NEWER if (!Enum.TryParse(prop.Value.ToString(), true, out var fc)) return new ErrorResponse($"Invalid friction_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply."); mat.frictionCombine = fc; changed.Add("frictionCombine"); #else if (!Enum.TryParse(prop.Value.ToString(), true, out var fc)) return new ErrorResponse($"Invalid friction_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply."); mat.frictionCombine = fc; changed.Add("frictionCombine"); #endif break; } case "bouncecombine": { #if UNITY_6000_0_OR_NEWER if (!Enum.TryParse(prop.Value.ToString(), true, out var bc)) return new ErrorResponse($"Invalid bounce_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply."); mat.bounceCombine = bc; changed.Add("bounceCombine"); #else if (!Enum.TryParse(prop.Value.ToString(), true, out var bc)) return new ErrorResponse($"Invalid bounce_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply."); mat.bounceCombine = bc; changed.Add("bounceCombine"); #endif break; } } } EditorUtility.SetDirty(mat); AssetDatabase.SaveAssets(); return new { success = true, message = $"Updated {changed.Count} property(ies) on 3D physics material at '{path}'.", data = new { path, changed } }; } private static readonly HashSet Valid2DMatKeys = new HashSet { "friction", "bounciness" }; private static object Configure2D(string path, JObject properties) { // Validate all keys before applying any changes var unknown = new List(); foreach (var prop in properties.Properties()) { string key = prop.Name.ToLowerInvariant().Replace("_", ""); if (!Valid2DMatKeys.Contains(key)) unknown.Add(prop.Name); } if (unknown.Count > 0) return new ErrorResponse( $"Unknown 2D physics material property(ies): {string.Join(", ", unknown)}."); var mat = AssetDatabase.LoadAssetAtPath(path); if (mat == null) return new ErrorResponse($"No 2D physics material found at: '{path}'."); Undo.RecordObject(mat, "Configure Physics Material 2D"); var changed = new List(); foreach (var prop in properties.Properties()) { string key = prop.Name.ToLowerInvariant().Replace("_", ""); switch (key) { case "friction": mat.friction = prop.Value.Value(); changed.Add("friction"); break; case "bounciness": mat.bounciness = prop.Value.Value(); changed.Add("bounciness"); break; } } EditorUtility.SetDirty(mat); AssetDatabase.SaveAssets(); return new { success = true, message = $"Updated {changed.Count} property(ies) on 2D physics material at '{path}'.", data = new { path, changed } }; } // ===================================================================== // Assign helpers // ===================================================================== private static Collider FindCollider3D(GameObject go, string colliderType, int? index = null) { if (!string.IsNullOrEmpty(colliderType)) { var type = UnityTypeResolver.ResolveComponent(colliderType); if (type != null && typeof(Collider).IsAssignableFrom(type)) { if (index.HasValue) { var components = go.GetComponents(type); if (index.Value < 0 || index.Value >= components.Length) return null; return components[index.Value] as Collider; } return go.GetComponent(type) as Collider; } return null; } if (index.HasValue) { var colliders = go.GetComponents(); if (index.Value < 0 || index.Value >= colliders.Length) return null; return colliders[index.Value]; } return go.GetComponent(); } private static Collider2D FindCollider2D(GameObject go, string colliderType, int? index = null) { if (!string.IsNullOrEmpty(colliderType)) { var type = UnityTypeResolver.ResolveComponent(colliderType); if (type != null && typeof(Collider2D).IsAssignableFrom(type)) { if (index.HasValue) { var components = go.GetComponents(type); if (index.Value < 0 || index.Value >= components.Length) return null; return components[index.Value] as Collider2D; } return go.GetComponent(type) as Collider2D; } return null; } if (index.HasValue) { var colliders = go.GetComponents(); if (index.Value < 0 || index.Value >= colliders.Length) return null; return colliders[index.Value]; } return go.GetComponent(); } // ===================================================================== // Folder helpers // ===================================================================== private static bool EnsureFolderExists(string folderPath, out string error) { error = null; if (string.IsNullOrWhiteSpace(folderPath)) { error = "Folder path is empty."; return false; } folderPath = folderPath.TrimEnd('/'); if (!folderPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) && !string.Equals(folderPath, "Assets", StringComparison.OrdinalIgnoreCase)) { error = "Folder path must be under Assets/."; return false; } if (AssetDatabase.IsValidFolder(folderPath)) return true; var parts = folderPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); string current = "Assets"; for (int i = 1; i < parts.Length; i++) { string next = current + "/" + parts[i]; if (!AssetDatabase.IsValidFolder(next)) { string guid = AssetDatabase.CreateFolder(current, parts[i]); if (string.IsNullOrEmpty(guid)) { error = $"Failed to create folder: {next}"; return false; } } current = next; } return true; } } }