using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; using UnityEngine; using MCPForUnity.Editor.Helpers; using MCPForUnity.Runtime.Helpers; namespace MCPForUnity.Editor.Tools.Physics { internal static class PhysicsQueryOps { public static object Raycast(JObject @params) { var p = new ToolParams(@params); string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant(); var originArr = p.GetRaw("origin") as JArray; if (originArr == null) return new ErrorResponse("'origin' parameter is required (array of floats)."); var dirArr = p.GetRaw("direction") as JArray; if (dirArr == null) return new ErrorResponse("'direction' parameter is required (array of floats)."); float maxDistance = p.GetFloat("max_distance") ?? Mathf.Infinity; if (dimension == "2d") return Raycast2D(originArr, dirArr, maxDistance, p); if (dimension != "3d") return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'."); return Raycast3D(originArr, dirArr, maxDistance, p); } private static object Raycast3D(JArray originArr, JArray dirArr, float maxDistance, ToolParams p) { if (originArr.Count < 3) return new ErrorResponse("3D raycast 'origin' requires [x, y, z]."); if (dirArr.Count < 3) return new ErrorResponse("3D raycast 'direction' requires [x, y, z]."); var origin = new Vector3( originArr[0].Value(), originArr[1].Value(), originArr[2].Value()); var direction = new Vector3( dirArr[0].Value(), dirArr[1].Value(), dirArr[2].Value()); int layerMask = ResolveLayerMask(p.Get("layer_mask")); QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal; string qti = p.Get("query_trigger_interaction"); if (!string.IsNullOrEmpty(qti)) { if (!System.Enum.TryParse(qti, true, out triggerInteraction)) return new ErrorResponse($"Invalid query_trigger_interaction: '{qti}'. Valid: UseGlobal, Ignore, Collide."); } UnityEngine.Physics.SyncTransforms(); bool hit = UnityEngine.Physics.Raycast(origin, direction, out RaycastHit hitInfo, maxDistance, layerMask, triggerInteraction); if (!hit) { return new { success = true, message = "Raycast did not hit anything.", data = new { hit = false } }; } return new { success = true, message = $"Raycast hit '{hitInfo.collider.gameObject.name}'.", data = new { hit = true, point = new[] { hitInfo.point.x, hitInfo.point.y, hitInfo.point.z }, normal = new[] { hitInfo.normal.x, hitInfo.normal.y, hitInfo.normal.z }, distance = hitInfo.distance, gameObject = hitInfo.collider.gameObject.name, instanceID = hitInfo.collider.gameObject.GetInstanceIDCompat(), collider_type = hitInfo.collider.GetType().Name } }; } private static object Raycast2D(JArray originArr, JArray dirArr, float maxDistance, ToolParams p) { if (originArr.Count < 2) return new ErrorResponse("2D raycast 'origin' requires [x, y]."); if (dirArr.Count < 2) return new ErrorResponse("2D raycast 'direction' requires [x, y]."); var origin = new Vector2(originArr[0].Value(), originArr[1].Value()); var direction = new Vector2(dirArr[0].Value(), dirArr[1].Value()); int layerMask = ResolveLayerMask(p.Get("layer_mask")); Physics2D.SyncTransforms(); var hit = Physics2D.Raycast(origin, direction, maxDistance, layerMask); if (hit.collider == null) { return new { success = true, message = "2D Raycast did not hit anything.", data = new { hit = false } }; } return new { success = true, message = $"2D Raycast hit '{hit.collider.gameObject.name}'.", data = new { hit = true, point = new[] { hit.point.x, hit.point.y }, normal = new[] { hit.normal.x, hit.normal.y }, distance = hit.distance, gameObject = hit.collider.gameObject.name, instanceID = hit.collider.gameObject.GetInstanceIDCompat(), collider_type = hit.collider.GetType().Name } }; } public static object Overlap(JObject @params) { var p = new ToolParams(@params); string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant(); string shape = p.Get("shape"); if (string.IsNullOrEmpty(shape)) return new ErrorResponse("'shape' parameter is required (sphere, box, capsule for 3D; circle, box, capsule for 2D)."); var posArr = p.GetRaw("position") as JArray; if (posArr == null) return new ErrorResponse("'position' parameter is required (array of floats)."); var sizeToken = p.GetRaw("size"); if (sizeToken == null) return new ErrorResponse("'size' parameter is required."); int layerMask = ResolveLayerMask(p.Get("layer_mask")); if (dimension == "2d") return Overlap2D(shape, posArr, sizeToken, layerMask); if (dimension != "3d") return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'."); return Overlap3D(shape, posArr, sizeToken, layerMask); } private static object Overlap3D(string shape, JArray posArr, JToken sizeToken, int layerMask) { if (posArr.Count < 3) return new ErrorResponse("3D overlap 'position' requires [x, y, z]."); var position = new Vector3( posArr[0].Value(), posArr[1].Value(), posArr[2].Value()); UnityEngine.Physics.SyncTransforms(); Collider[] results; switch (shape.ToLowerInvariant()) { case "sphere": { float radius = sizeToken.Value(); results = UnityEngine.Physics.OverlapSphere(position, radius, layerMask); break; } case "box": { Vector3 halfExtents; if (sizeToken is JArray sizeArr && sizeArr.Count >= 3) halfExtents = new Vector3( sizeArr[0].Value(), sizeArr[1].Value(), sizeArr[2].Value()); else return new ErrorResponse("3D box overlap 'size' requires [halfX, halfY, halfZ]."); results = UnityEngine.Physics.OverlapBox(position, halfExtents, Quaternion.identity, layerMask); break; } case "capsule": { if (sizeToken is JObject capsuleObj) { float radius = capsuleObj["radius"]?.Value() ?? 0.5f; float height = capsuleObj["height"]?.Value() ?? 2f; int direction = capsuleObj["direction"]?.Value() ?? 1; Vector3 point0, point1; float halfHeight = Mathf.Max(0, height / 2f - radius); switch (direction) { case 0: // X point0 = position + Vector3.left * halfHeight; point1 = position + Vector3.right * halfHeight; break; case 2: // Z point0 = position + Vector3.back * halfHeight; point1 = position + Vector3.forward * halfHeight; break; default: // Y (1) point0 = position + Vector3.down * halfHeight; point1 = position + Vector3.up * halfHeight; break; } results = UnityEngine.Physics.OverlapCapsule(point0, point1, radius, layerMask); } else { return new ErrorResponse("3D capsule overlap 'size' requires {radius, height, direction}."); } break; } default: return new ErrorResponse($"Unknown 3D shape: '{shape}'. Valid: sphere, box, capsule."); } return FormatOverlapResults(results, "3D"); } private static object Overlap2D(string shape, JArray posArr, JToken sizeToken, int layerMask) { if (posArr.Count < 2) return new ErrorResponse("2D overlap 'position' requires [x, y]."); var position = new Vector2(posArr[0].Value(), posArr[1].Value()); Physics2D.SyncTransforms(); Collider2D[] results; switch (shape.ToLowerInvariant()) { case "circle": { float radius = sizeToken.Value(); results = Physics2D.OverlapCircleAll(position, radius, layerMask); break; } case "box": { Vector2 size; if (sizeToken is JArray sizeArr && sizeArr.Count >= 2) size = new Vector2(sizeArr[0].Value(), sizeArr[1].Value()); else return new ErrorResponse("2D box overlap 'size' requires [width, height]."); results = Physics2D.OverlapBoxAll(position, size, 0f, layerMask); break; } case "capsule": { if (sizeToken is JObject capsuleObj) { float sizeX = capsuleObj["width"]?.Value() ?? 1f; float sizeY = capsuleObj["height"]?.Value() ?? 2f; var dir = CapsuleDirection2D.Vertical; string dirStr = capsuleObj["direction"]?.ToString(); if (dirStr != null && dirStr.ToLowerInvariant() == "horizontal") dir = CapsuleDirection2D.Horizontal; results = Physics2D.OverlapCapsuleAll(position, new Vector2(sizeX, sizeY), dir, 0f, layerMask); } else { return new ErrorResponse("2D capsule overlap 'size' requires {width, height, direction}."); } break; } default: return new ErrorResponse($"Unknown 2D shape: '{shape}'. Valid: circle, box, capsule."); } return FormatOverlapResults2D(results, "2D"); } private static object FormatOverlapResults(Collider[] results, string dimension) { var colliders = new List(); foreach (var col in results) { colliders.Add(new { gameObject = col.gameObject.name, instanceID = col.gameObject.GetInstanceIDCompat(), collider_type = col.GetType().Name }); } return new { success = true, message = $"Overlap query found {colliders.Count} collider(s) ({dimension}).", data = new { colliders } }; } private static object FormatOverlapResults2D(Collider2D[] results, string dimension) { var colliders = new List(); foreach (var col in results) { colliders.Add(new { gameObject = col.gameObject.name, instanceID = col.gameObject.GetInstanceIDCompat(), collider_type = col.GetType().Name }); } return new { success = true, message = $"Overlap query found {colliders.Count} collider(s) ({dimension}).", data = new { colliders } }; } public static object Shapecast(JObject @params) { var p = new ToolParams(@params); string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant(); string shape = p.Get("shape"); if (string.IsNullOrEmpty(shape)) return new ErrorResponse("'shape' parameter is required (sphere, box, capsule for 3D; circle, box, capsule for 2D)."); var originArr = p.GetRaw("origin") as JArray; if (originArr == null) return new ErrorResponse("'origin' parameter is required (array of floats)."); var dirArr = p.GetRaw("direction") as JArray; if (dirArr == null) return new ErrorResponse("'direction' parameter is required (array of floats)."); float maxDistance = p.GetFloat("max_distance") ?? Mathf.Infinity; if (dimension == "2d") return Shapecast2D(shape, originArr, dirArr, maxDistance, p); if (dimension != "3d") return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'."); return Shapecast3D(shape, originArr, dirArr, maxDistance, p); } private static object Shapecast3D(string shape, JArray originArr, JArray dirArr, float maxDistance, ToolParams p) { if (originArr.Count < 3) return new ErrorResponse("3D shapecast 'origin' requires [x, y, z]."); if (dirArr.Count < 3) return new ErrorResponse("3D shapecast 'direction' requires [x, y, z]."); var origin = new Vector3( originArr[0].Value(), originArr[1].Value(), originArr[2].Value()); var direction = new Vector3( dirArr[0].Value(), dirArr[1].Value(), dirArr[2].Value()); int layerMask = ResolveLayerMask(p.Get("layer_mask")); QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal; string qti = p.Get("query_trigger_interaction"); if (!string.IsNullOrEmpty(qti)) { if (!System.Enum.TryParse(qti, true, out triggerInteraction)) return new ErrorResponse($"Invalid query_trigger_interaction: '{qti}'. Valid: UseGlobal, Ignore, Collide."); } var sizeToken = p.GetRaw("size"); if (sizeToken == null) return new ErrorResponse("'size' parameter is required."); UnityEngine.Physics.SyncTransforms(); bool hit; RaycastHit hitInfo; switch (shape.ToLowerInvariant()) { case "sphere": { float radius = sizeToken.Value(); hit = UnityEngine.Physics.SphereCast(origin, radius, direction, out hitInfo, maxDistance, layerMask, triggerInteraction); break; } case "box": { Vector3 halfExtents; if (sizeToken is JArray sizeArr && sizeArr.Count >= 3) halfExtents = new Vector3( sizeArr[0].Value(), sizeArr[1].Value(), sizeArr[2].Value()); else return new ErrorResponse("3D box shapecast 'size' requires [halfX, halfY, halfZ]."); hit = UnityEngine.Physics.BoxCast(origin, halfExtents, direction, out hitInfo, Quaternion.identity, maxDistance, layerMask, triggerInteraction); break; } case "capsule": { float radius = sizeToken.Value(); Vector3 pt1, pt2; var pt1Arr = p.GetRaw("point1") as JArray; var pt2Arr = p.GetRaw("point2") as JArray; if (pt1Arr != null && pt1Arr.Count >= 3 && pt2Arr != null && pt2Arr.Count >= 3) { pt1 = new Vector3(pt1Arr[0].Value(), pt1Arr[1].Value(), pt1Arr[2].Value()); pt2 = new Vector3(pt2Arr[0].Value(), pt2Arr[1].Value(), pt2Arr[2].Value()); } else { float height = p.GetFloat("height") ?? 2f; int capsuleDirection = p.GetInt("capsule_direction") ?? 1; float halfHeight = Mathf.Max(0, height / 2f - radius); switch (capsuleDirection) { case 0: // X pt1 = origin + Vector3.left * halfHeight; pt2 = origin + Vector3.right * halfHeight; break; case 2: // Z pt1 = origin + Vector3.back * halfHeight; pt2 = origin + Vector3.forward * halfHeight; break; default: // Y (1) pt1 = origin + Vector3.down * halfHeight; pt2 = origin + Vector3.up * halfHeight; break; } } hit = UnityEngine.Physics.CapsuleCast(pt1, pt2, radius, direction, out hitInfo, maxDistance, layerMask, triggerInteraction); break; } default: return new ErrorResponse($"Unknown 3D shape: '{shape}'. Valid: sphere, box, capsule."); } if (!hit) { return new { success = true, message = "Shapecast did not hit anything.", data = new { hit = false } }; } return new { success = true, message = $"Shapecast hit '{hitInfo.collider.gameObject.name}'.", data = new { hit = true, point = new[] { hitInfo.point.x, hitInfo.point.y, hitInfo.point.z }, normal = new[] { hitInfo.normal.x, hitInfo.normal.y, hitInfo.normal.z }, distance = hitInfo.distance, gameObject = hitInfo.collider.gameObject.name, instanceID = hitInfo.collider.gameObject.GetInstanceIDCompat(), collider_type = hitInfo.collider.GetType().Name } }; } private static object Shapecast2D(string shape, JArray originArr, JArray dirArr, float maxDistance, ToolParams p) { if (originArr.Count < 2) return new ErrorResponse("2D shapecast 'origin' requires [x, y]."); if (dirArr.Count < 2) return new ErrorResponse("2D shapecast 'direction' requires [x, y]."); var origin = new Vector2(originArr[0].Value(), originArr[1].Value()); var direction = new Vector2(dirArr[0].Value(), dirArr[1].Value()); int layerMask = ResolveLayerMask(p.Get("layer_mask")); float angle = p.GetFloat("angle") ?? 0f; var sizeToken = p.GetRaw("size"); if (sizeToken == null) return new ErrorResponse("'size' parameter is required."); Physics2D.SyncTransforms(); RaycastHit2D hit; switch (shape.ToLowerInvariant()) { case "circle": { float radius = sizeToken.Value(); hit = Physics2D.CircleCast(origin, radius, direction, maxDistance, layerMask); break; } case "box": { Vector2 size; if (sizeToken is JArray sizeArr && sizeArr.Count >= 2) size = new Vector2(sizeArr[0].Value(), sizeArr[1].Value()); else return new ErrorResponse("2D box shapecast 'size' requires [width, height]."); hit = Physics2D.BoxCast(origin, size, angle, direction, maxDistance, layerMask); break; } case "capsule": { if (sizeToken is JObject sizeObj) { var capsuleSize = new Vector2( sizeObj["width"]?.Value() ?? 1f, sizeObj["height"]?.Value() ?? 2f); var capsuleDir = CapsuleDirection2D.Vertical; string dirStr = sizeObj["direction"]?.ToString(); if (dirStr != null && dirStr.ToLowerInvariant() == "horizontal") capsuleDir = CapsuleDirection2D.Horizontal; hit = Physics2D.CapsuleCast(origin, capsuleSize, capsuleDir, angle, direction, maxDistance, layerMask); } else { return new ErrorResponse("2D capsule shapecast 'size' requires {width, height, direction}."); } break; } default: return new ErrorResponse($"Unknown 2D shape: '{shape}'. Valid: circle, box, capsule."); } if (hit.collider == null) { return new { success = true, message = "2D Shapecast did not hit anything.", data = new { hit = false } }; } return new { success = true, message = $"2D Shapecast hit '{hit.collider.gameObject.name}'.", data = new { hit = true, point = new[] { hit.point.x, hit.point.y }, normal = new[] { hit.normal.x, hit.normal.y }, distance = hit.distance, gameObject = hit.collider.gameObject.name, instanceID = hit.collider.gameObject.GetInstanceIDCompat(), collider_type = hit.collider.GetType().Name } }; } public static object RaycastAll(JObject @params) { var p = new ToolParams(@params); string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant(); var originArr = p.GetRaw("origin") as JArray; if (originArr == null) return new ErrorResponse("'origin' parameter is required (array of floats)."); var dirArr = p.GetRaw("direction") as JArray; if (dirArr == null) return new ErrorResponse("'direction' parameter is required (array of floats)."); float maxDistance = p.GetFloat("max_distance") ?? Mathf.Infinity; if (dimension == "2d") return RaycastAll2D(originArr, dirArr, maxDistance, p); if (dimension != "3d") return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'."); return RaycastAll3D(originArr, dirArr, maxDistance, p); } private static object RaycastAll3D(JArray originArr, JArray dirArr, float maxDistance, ToolParams p) { if (originArr.Count < 3) return new ErrorResponse("3D RaycastAll 'origin' requires [x, y, z]."); if (dirArr.Count < 3) return new ErrorResponse("3D RaycastAll 'direction' requires [x, y, z]."); var origin = new Vector3( originArr[0].Value(), originArr[1].Value(), originArr[2].Value()); var direction = new Vector3( dirArr[0].Value(), dirArr[1].Value(), dirArr[2].Value()); int layerMask = ResolveLayerMask(p.Get("layer_mask")); QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal; string qti = p.Get("query_trigger_interaction"); if (!string.IsNullOrEmpty(qti)) { if (!System.Enum.TryParse(qti, true, out triggerInteraction)) return new ErrorResponse($"Invalid query_trigger_interaction: '{qti}'. Valid: UseGlobal, Ignore, Collide."); } UnityEngine.Physics.SyncTransforms(); RaycastHit[] hits = UnityEngine.Physics.RaycastAll(origin, direction, maxDistance, layerMask, triggerInteraction); System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); var hitsArray = new List(); foreach (var h in hits) { hitsArray.Add(new { point = new[] { h.point.x, h.point.y, h.point.z }, normal = new[] { h.normal.x, h.normal.y, h.normal.z }, distance = h.distance, gameObject = h.collider.gameObject.name, instanceID = h.collider.gameObject.GetInstanceIDCompat(), collider_type = h.collider.GetType().Name }); } return new { success = true, message = $"RaycastAll found {hits.Length} hit(s).", data = new { hit_count = hits.Length, hits = hitsArray } }; } private static object RaycastAll2D(JArray originArr, JArray dirArr, float maxDistance, ToolParams p) { if (originArr.Count < 2) return new ErrorResponse("2D RaycastAll 'origin' requires [x, y]."); if (dirArr.Count < 2) return new ErrorResponse("2D RaycastAll 'direction' requires [x, y]."); var origin = new Vector2(originArr[0].Value(), originArr[1].Value()); var direction = new Vector2(dirArr[0].Value(), dirArr[1].Value()); int layerMask = ResolveLayerMask(p.Get("layer_mask")); Physics2D.SyncTransforms(); RaycastHit2D[] hits = Physics2D.RaycastAll(origin, direction, maxDistance, layerMask); var hitsArray = new List(); foreach (var h in hits) { hitsArray.Add(new { point = new[] { h.point.x, h.point.y }, normal = new[] { h.normal.x, h.normal.y }, distance = h.distance, gameObject = h.collider.gameObject.name, instanceID = h.collider.gameObject.GetInstanceIDCompat(), collider_type = h.collider.GetType().Name }); } return new { success = true, message = $"RaycastAll found {hits.Length} hit(s).", data = new { hit_count = hits.Length, hits = hitsArray } }; } public static object Linecast(JObject @params) { var p = new ToolParams(@params); string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant(); var startArr = p.GetRaw("start") as JArray; if (startArr == null) return new ErrorResponse("'start' parameter is required (array of floats)."); var endArr = p.GetRaw("end") as JArray; if (endArr == null) return new ErrorResponse("'end' parameter is required (array of floats)."); if (dimension == "2d") return Linecast2D(startArr, endArr, p); if (dimension != "3d") return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'."); return Linecast3D(startArr, endArr, p); } private static object Linecast3D(JArray startArr, JArray endArr, ToolParams p) { if (startArr.Count < 3) return new ErrorResponse("3D linecast 'start' requires [x, y, z]."); if (endArr.Count < 3) return new ErrorResponse("3D linecast 'end' requires [x, y, z]."); var start = new Vector3( startArr[0].Value(), startArr[1].Value(), startArr[2].Value()); var end = new Vector3( endArr[0].Value(), endArr[1].Value(), endArr[2].Value()); int layerMask = ResolveLayerMask(p.Get("layer_mask")); QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal; string qti = p.Get("query_trigger_interaction"); if (!string.IsNullOrEmpty(qti)) { if (!System.Enum.TryParse(qti, true, out triggerInteraction)) return new ErrorResponse($"Invalid query_trigger_interaction: '{qti}'. Valid: UseGlobal, Ignore, Collide."); } UnityEngine.Physics.SyncTransforms(); bool hit = UnityEngine.Physics.Linecast(start, end, out RaycastHit hitInfo, layerMask, triggerInteraction); if (!hit) { return new { success = true, message = "Linecast did not hit anything.", data = new { hit = false } }; } return new { success = true, message = $"Linecast hit '{hitInfo.collider.gameObject.name}'.", data = new { hit = true, point = new[] { hitInfo.point.x, hitInfo.point.y, hitInfo.point.z }, normal = new[] { hitInfo.normal.x, hitInfo.normal.y, hitInfo.normal.z }, distance = hitInfo.distance, gameObject = hitInfo.collider.gameObject.name, instanceID = hitInfo.collider.gameObject.GetInstanceIDCompat(), collider_type = hitInfo.collider.GetType().Name } }; } private static object Linecast2D(JArray startArr, JArray endArr, ToolParams p) { if (startArr.Count < 2) return new ErrorResponse("2D linecast 'start' requires [x, y]."); if (endArr.Count < 2) return new ErrorResponse("2D linecast 'end' requires [x, y]."); var start = new Vector2(startArr[0].Value(), startArr[1].Value()); var end = new Vector2(endArr[0].Value(), endArr[1].Value()); int layerMask = ResolveLayerMask(p.Get("layer_mask")); Physics2D.SyncTransforms(); var hit = Physics2D.Linecast(start, end, layerMask); if (hit.collider == null) { return new { success = true, message = "2D Linecast did not hit anything.", data = new { hit = false } }; } return new { success = true, message = $"2D Linecast hit '{hit.collider.gameObject.name}'.", data = new { hit = true, point = new[] { hit.point.x, hit.point.y }, normal = new[] { hit.normal.x, hit.normal.y }, distance = hit.distance, gameObject = hit.collider.gameObject.name, instanceID = hit.collider.gameObject.GetInstanceIDCompat(), collider_type = hit.collider.GetType().Name } }; } private static int ResolveLayerMask(string layerMaskStr) { if (string.IsNullOrEmpty(layerMaskStr)) return ~0; // All layers if (int.TryParse(layerMaskStr, out int mask)) return mask; int layer = LayerMask.NameToLayer(layerMaskStr); if (layer >= 0) return 1 << layer; throw new ArgumentException($"Unknown layer name: '{layerMaskStr}'. Use a valid layer name or integer mask."); } } }