using System; using System.Collections.Generic; using System.Reflection; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using MCPForUnity.Editor.Helpers; using MCPForUnity.Runtime.Helpers; namespace MCPForUnity.Editor.Tools.Physics { internal static class JointOps { private static readonly Dictionary JointTypes3D = new Dictionary { { "fixed", typeof(FixedJoint) }, { "hinge", typeof(HingeJoint) }, { "spring", typeof(SpringJoint) }, { "character", typeof(CharacterJoint) }, { "configurable", typeof(ConfigurableJoint) } }; private static readonly Dictionary JointTypes2D = new Dictionary { { "distance", typeof(DistanceJoint2D) }, { "fixed", typeof(FixedJoint2D) }, { "friction", typeof(FrictionJoint2D) }, { "hinge", typeof(HingeJoint2D) }, { "relative", typeof(RelativeJoint2D) }, { "slider", typeof(SliderJoint2D) }, { "spring", typeof(SpringJoint2D) }, { "target", typeof(TargetJoint2D) }, { "wheel", typeof(WheelJoint2D) } }; public static object AddJoint(JObject @params) { var p = new ToolParams(@params); var targetResult = p.GetRequired("target"); var errorObj = targetResult.GetOrError(out string targetStr); if (errorObj != null) return errorObj; var jointTypeResult = p.GetRequired("joint_type"); errorObj = jointTypeResult.GetOrError(out string jointTypeStr); if (errorObj != null) return errorObj; string searchMethod = p.Get("search_method"); GameObject go = FindTarget(@params["target"], searchMethod); if (go == null) return new ErrorResponse($"Target GameObject '{targetStr}' not found."); string dimensionParam = p.Get("dimension")?.ToLowerInvariant(); bool is3D = go.GetComponent() != null; bool has2DRb = go.GetComponent() != null; bool is2D; if (dimensionParam == "2d") { if (!has2DRb) return new ErrorResponse($"Target '{go.name}' has no Rigidbody2D."); is2D = true; } else if (dimensionParam == "3d") { if (!is3D) return new ErrorResponse($"Target '{go.name}' has no Rigidbody."); is2D = false; } else if (!string.IsNullOrEmpty(dimensionParam)) { return new ErrorResponse($"Invalid dimension: '{dimensionParam}'. Use '3d' or '2d'."); } else { // Auto-detect; if both, prefer 3D (3D is the default physics) is2D = has2DRb && !is3D; } if (!is2D && !is3D) return new ErrorResponse($"Target '{go.name}' has no Rigidbody or Rigidbody2D. Add one before adding a joint."); string key = jointTypeStr.ToLowerInvariant(); var typeMap = is2D ? JointTypes2D : JointTypes3D; if (!typeMap.TryGetValue(key, out Type jointComponentType)) { string validTypes = string.Join(", ", typeMap.Keys); string dimension = is2D ? "2D" : "3D"; return new ErrorResponse($"Unknown {dimension} joint type: '{jointTypeStr}'. Valid types: {validTypes}."); } // Validate connected body before mutating the scene string connectedBodyTarget = p.Get("connected_body"); GameObject connectedGo = null; if (!string.IsNullOrEmpty(connectedBodyTarget)) { connectedGo = FindTarget(new JValue(connectedBodyTarget), searchMethod); if (connectedGo == null) return new ErrorResponse($"Connected body GameObject '{connectedBodyTarget}' not found."); if (is2D) { if (connectedGo.GetComponent() == null) return new ErrorResponse($"Connected body '{connectedGo.name}' has no Rigidbody2D."); } else { if (connectedGo.GetComponent() == null) return new ErrorResponse($"Connected body '{connectedGo.name}' has no Rigidbody."); } } var joint = Undo.AddComponent(go, jointComponentType); if (joint == null) return new ErrorResponse($"Failed to add {jointComponentType.Name} to '{go.name}'."); // Set connected body now that the joint is created if (connectedGo != null) { if (is2D) ((Joint2D)joint).connectedBody = connectedGo.GetComponent(); else ((Joint)joint).connectedBody = connectedGo.GetComponent(); } // Set properties via reflection if provided var properties = p.GetRaw("properties") as JObject; if (properties != null) { SetPropertiesViaReflection(joint, properties); } EditorUtility.SetDirty(go); return new { success = true, message = $"{jointComponentType.Name} added to '{go.name}'.", data = new { jointType = jointComponentType.Name, instanceID = joint.GetInstanceIDCompat(), gameObjectInstanceID = go.GetInstanceIDCompat() } }; } public static object ConfigureJoint(JObject @params) { var p = new ToolParams(@params); var targetResult = p.GetRequired("target"); var errorObj = targetResult.GetOrError(out string targetStr); if (errorObj != null) return errorObj; string searchMethod = p.Get("search_method"); GameObject go = FindTarget(@params["target"], searchMethod); if (go == null) return new ErrorResponse($"Target GameObject '{targetStr}' not found."); string jointTypeStr = p.Get("joint_type"); int? componentIndex = ParamCoercion.CoerceIntNullable(@params["componentIndex"] ?? @params["component_index"]); if (componentIndex.HasValue && string.IsNullOrEmpty(jointTypeStr)) return new ErrorResponse("component_index requires joint_type to be specified."); Component joint = ResolveJoint(go, jointTypeStr, componentIndex, out int foundCount); if (joint == null) { if (componentIndex.HasValue && foundCount >= 0) return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {foundCount} joint(s) on '{go.name}'."); if (!string.IsNullOrEmpty(jointTypeStr)) return new ErrorResponse($"No joint of type '{jointTypeStr}' found on '{go.name}'."); // Check if multiple joints exist to give a better error var joints3D = go.GetComponents(); var joints2D = go.GetComponents(); int total = joints3D.Length + joints2D.Length; if (total > 1) return new ErrorResponse($"Multiple joints found on '{go.name}' ({total} total). Specify 'joint_type' to target a specific joint."); return new ErrorResponse($"No joint found on '{go.name}'."); } Undo.RecordObject(joint, $"Configure {joint.GetType().Name}"); var configured = new List(); // Motor configuration (HingeJoint) var motorToken = p.GetRaw("motor") as JObject; if (motorToken != null) { if (joint is HingeJoint hingeForMotor) { var motor = hingeForMotor.motor; if (motorToken["targetVelocity"] != null) motor.targetVelocity = motorToken["targetVelocity"].Value(); if (motorToken["force"] != null) motor.force = motorToken["force"].Value(); if (motorToken["freeSpin"] != null) motor.freeSpin = motorToken["freeSpin"].Value(); hingeForMotor.motor = motor; hingeForMotor.useMotor = true; configured.Add("motor"); } else { return new ErrorResponse($"Motor configuration is only supported on HingeJoint, not {joint.GetType().Name}."); } } // Limits configuration (HingeJoint) var limitsToken = p.GetRaw("limits") as JObject; if (limitsToken != null) { if (joint is HingeJoint hingeForLimits) { var limits = hingeForLimits.limits; if (limitsToken["min"] != null) limits.min = limitsToken["min"].Value(); if (limitsToken["max"] != null) limits.max = limitsToken["max"].Value(); if (limitsToken["bounciness"] != null) limits.bounciness = limitsToken["bounciness"].Value(); hingeForLimits.limits = limits; hingeForLimits.useLimits = true; configured.Add("limits"); } else { return new ErrorResponse($"Limits configuration is only supported on HingeJoint, not {joint.GetType().Name}."); } } // Spring configuration (HingeJoint / SpringJoint) var springToken = p.GetRaw("spring") as JObject; if (springToken != null) { if (joint is HingeJoint hingeForSpring) { var spring = hingeForSpring.spring; if (springToken["spring"] != null) spring.spring = springToken["spring"].Value(); if (springToken["damper"] != null) spring.damper = springToken["damper"].Value(); if (springToken["targetPosition"] != null) spring.targetPosition = springToken["targetPosition"].Value(); hingeForSpring.spring = spring; hingeForSpring.useSpring = true; configured.Add("spring"); } else if (joint is SpringJoint springJoint) { if (springToken["spring"] != null) springJoint.spring = springToken["spring"].Value(); if (springToken["damper"] != null) springJoint.damper = springToken["damper"].Value(); if (springToken["targetPosition"] != null) springJoint.minDistance = springToken["targetPosition"].Value(); configured.Add("spring"); } else { return new ErrorResponse($"Spring configuration is only supported on HingeJoint/SpringJoint, not {joint.GetType().Name}."); } } // Drive configuration (ConfigurableJoint) var driveToken = p.GetRaw("drive") as JObject; if (driveToken != null) { if (joint is ConfigurableJoint configJoint) { var xDriveToken = driveToken["xDrive"] as JObject; if (xDriveToken != null) { var xDrive = configJoint.xDrive; if (xDriveToken["positionSpring"] != null) xDrive.positionSpring = xDriveToken["positionSpring"].Value(); if (xDriveToken["positionDamper"] != null) xDrive.positionDamper = xDriveToken["positionDamper"].Value(); if (xDriveToken["maximumForce"] != null) xDrive.maximumForce = xDriveToken["maximumForce"].Value(); configJoint.xDrive = xDrive; } configured.Add("drive"); } else { return new ErrorResponse($"Drive configuration is only supported on ConfigurableJoint, not {joint.GetType().Name}."); } } // Direct property setting var properties = p.GetRaw("properties") as JObject; if (properties != null) { SetPropertiesViaReflection(joint, properties); configured.Add("properties"); } EditorUtility.SetDirty(joint); return new { success = true, message = $"Configured {joint.GetType().Name} on '{go.name}': {string.Join(", ", configured)}.", data = new { jointType = joint.GetType().Name, configured, instanceID = joint.GetInstanceIDCompat() } }; } public static object RemoveJoint(JObject @params) { var p = new ToolParams(@params); var targetResult = p.GetRequired("target"); var errorObj = targetResult.GetOrError(out string targetStr); if (errorObj != null) return errorObj; string searchMethod = p.Get("search_method"); GameObject go = FindTarget(@params["target"], searchMethod); if (go == null) return new ErrorResponse($"Target GameObject '{targetStr}' not found."); string jointTypeStr = p.Get("joint_type"); int? componentIndex = ParamCoercion.CoerceIntNullable(@params["componentIndex"] ?? @params["component_index"]); if (componentIndex.HasValue && string.IsNullOrEmpty(jointTypeStr)) return new ErrorResponse("component_index requires joint_type to be specified."); var jointsToRemove = new List(); if (!string.IsNullOrEmpty(jointTypeStr)) { // Remove specific joint type bool is2D = go.GetComponent() != null; var typeMap = is2D ? JointTypes2D : JointTypes3D; string key = jointTypeStr.ToLowerInvariant(); if (!typeMap.TryGetValue(key, out Type jointComponentType)) { string validTypes = string.Join(", ", typeMap.Keys); string dimension = is2D ? "2D" : "3D"; return new ErrorResponse($"Unknown {dimension} joint type: '{jointTypeStr}'. Valid types: {validTypes}."); } var components = go.GetComponents(jointComponentType); if (componentIndex.HasValue) { if (componentIndex.Value < 0 || componentIndex.Value >= components.Length) return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {components.Length} '{jointComponentType.Name}' joint(s) on '{go.name}'."); jointsToRemove.Add(components[componentIndex.Value]); } else { jointsToRemove.AddRange(components); } } else { // Remove ALL joints (both 3D and 2D) var joints3D = go.GetComponents(); var joints2D = go.GetComponents(); jointsToRemove.AddRange(joints3D); jointsToRemove.AddRange(joints2D); } if (jointsToRemove.Count == 0) { string typeInfo = string.IsNullOrEmpty(jointTypeStr) ? "" : $" of type '{jointTypeStr}'"; return new ErrorResponse($"No joints{typeInfo} found on '{go.name}'."); } int removedCount = 0; foreach (var joint in jointsToRemove) { Undo.DestroyObjectImmediate(joint); removedCount++; } EditorUtility.SetDirty(go); return new { success = true, message = $"Removed {removedCount} joint(s) from '{go.name}'.", data = new { removedCount, gameObjectInstanceID = go.GetInstanceIDCompat() } }; } private static GameObject FindTarget(JToken targetToken, string searchMethod) { if (targetToken == null) return null; if (targetToken.Type == JTokenType.Integer) { int instanceId = targetToken.Value(); return GameObjectLookup.FindById(instanceId); } string targetStr = targetToken.ToString(); if (int.TryParse(targetStr, out int parsedId)) { var byId = GameObjectLookup.FindById(parsedId); if (byId != null) return byId; } return GameObjectLookup.FindByTarget(targetToken, searchMethod ?? "by_name", true); } private static Component ResolveJoint(GameObject go, string jointTypeStr, int? index, out int foundCount) { foundCount = -1; if (!string.IsNullOrEmpty(jointTypeStr)) { bool is2D = go.GetComponent() != null; var typeMap = is2D ? JointTypes2D : JointTypes3D; string key = jointTypeStr.ToLowerInvariant(); if (typeMap.TryGetValue(key, out Type jointType)) { if (index.HasValue) { var components = go.GetComponents(jointType); foundCount = components.Length; if (index.Value < 0 || index.Value >= components.Length) return null; return components[index.Value]; } return go.GetComponent(jointType); } return null; } // Auto-detect: find the single joint on the GO var joints3D = go.GetComponents(); var joints2D = go.GetComponents(); int totalCount = joints3D.Length + joints2D.Length; if (totalCount == 1) return joints3D.Length == 1 ? (Component)joints3D[0] : joints2D[0]; return null; } private static void SetPropertiesViaReflection(Component component, JObject properties) { var type = component.GetType(); foreach (var prop in properties.Properties()) { var propInfo = type.GetProperty(prop.Name, BindingFlags.Public | BindingFlags.Instance); if (propInfo != null && propInfo.CanWrite) { try { object value = ConvertValue(prop.Value, propInfo.PropertyType); propInfo.SetValue(component, value); } catch (Exception ex) { McpLog.Warn($"[JointOps] Failed to set property '{prop.Name}': {ex.Message}"); } continue; } var fieldInfo = type.GetField(prop.Name, BindingFlags.Public | BindingFlags.Instance); if (fieldInfo != null) { try { object value = ConvertValue(prop.Value, fieldInfo.FieldType); fieldInfo.SetValue(component, value); } catch (Exception ex) { McpLog.Warn($"[JointOps] Failed to set field '{prop.Name}': {ex.Message}"); } } } } private static object ConvertValue(JToken token, Type targetType) { if (targetType == typeof(float)) return token.Value(); if (targetType == typeof(int)) return token.Value(); if (targetType == typeof(bool)) return token.Value(); if (targetType == typeof(string)) return token.Value(); if (targetType == typeof(Vector3)) { var arr = token as JArray; if (arr != null && arr.Count >= 3) return new Vector3(arr[0].Value(), arr[1].Value(), arr[2].Value()); } if (targetType == typeof(Vector2)) { var arr = token as JArray; if (arr != null && arr.Count >= 2) return new Vector2(arr[0].Value(), arr[1].Value()); } return token.ToObject(targetType); } } }