|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Linq; |
| 4 | +using MCPForUnity.Editor.Helpers; |
| 5 | +using Newtonsoft.Json.Linq; |
| 6 | +using UnityEditor; |
| 7 | +using UnityEngine; |
| 8 | + |
| 9 | +namespace MCPForUnity.Editor.Tools |
| 10 | +{ |
| 11 | + /// <summary> |
| 12 | + /// Simulates player input during play mode for AI game testing. |
| 13 | + /// Supports key presses, mouse clicks, axis simulation, and state queries. |
| 14 | + /// </summary> |
| 15 | + [McpForUnityTool("simulate_input", AutoRegister = false)] |
| 16 | + public static class ManageInput |
| 17 | + { |
| 18 | + public static object HandleCommand(JObject @params) |
| 19 | + { |
| 20 | + if (@params == null) |
| 21 | + return new ErrorResponse("Parameters cannot be null."); |
| 22 | + |
| 23 | + var p = new ToolParams(@params); |
| 24 | + var actionResult = p.GetRequired("action"); |
| 25 | + if (!actionResult.IsSuccess) |
| 26 | + return new ErrorResponse(actionResult.ErrorMessage); |
| 27 | + |
| 28 | + string action = actionResult.Value.ToLowerInvariant(); |
| 29 | + |
| 30 | + switch (action) |
| 31 | + { |
| 32 | + case "send_key": |
| 33 | + return HandleSendKey(p); |
| 34 | + case "send_mouse_click": |
| 35 | + return HandleSendMouseClick(p); |
| 36 | + case "send_mouse_move": |
| 37 | + return HandleSendMouseMove(p); |
| 38 | + case "send_sequence": |
| 39 | + return HandleSendSequence(p, @params); |
| 40 | + case "get_state": |
| 41 | + return HandleGetState(p); |
| 42 | + default: |
| 43 | + return new ErrorResponse( |
| 44 | + $"Unknown action: '{action}'. Valid actions: send_key, send_mouse_click, send_mouse_move, send_sequence, get_state."); |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + private static object RequirePlayMode() |
| 49 | + { |
| 50 | + if (!EditorApplication.isPlaying) |
| 51 | + return new ErrorResponse("Input simulation requires Play Mode. Use manage_editor action='play' first."); |
| 52 | + return null; |
| 53 | + } |
| 54 | + |
| 55 | + private static object HandleSendKey(ToolParams p) |
| 56 | + { |
| 57 | + var guard = RequirePlayMode(); |
| 58 | + if (guard != null) return guard; |
| 59 | + |
| 60 | + string key = p.Get("key"); |
| 61 | + if (string.IsNullOrEmpty(key)) |
| 62 | + return new ErrorResponse("'key' parameter is required (e.g., 'W', 'Space', 'Mouse0')."); |
| 63 | + |
| 64 | + int holdDurationMs = p.GetInt("holdDuration") ?? 100; |
| 65 | + bool press = p.GetBool("press", true); |
| 66 | + bool release = p.GetBool("release", true); |
| 67 | + |
| 68 | + var simulator = GetOrCreateSimulator(); |
| 69 | + if (simulator == null) |
| 70 | + return new ErrorResponse("Failed to create InputSimulator in the scene."); |
| 71 | + |
| 72 | + simulator.QueueKeyAction(key, holdDurationMs, press, release); |
| 73 | + |
| 74 | + return new SuccessResponse( |
| 75 | + $"Queued key '{key}' (hold {holdDurationMs}ms, press={press}, release={release}).", |
| 76 | + new { key, holdDurationMs, press, release }); |
| 77 | + } |
| 78 | + |
| 79 | + private static object HandleSendMouseClick(ToolParams p) |
| 80 | + { |
| 81 | + var guard = RequirePlayMode(); |
| 82 | + if (guard != null) return guard; |
| 83 | + |
| 84 | + float x = (float)(p.GetInt("x") ?? 0); |
| 85 | + float y = (float)(p.GetInt("y") ?? 0); |
| 86 | + int button = p.GetInt("button") ?? 0; // 0=left, 1=right, 2=middle |
| 87 | + |
| 88 | + var simulator = GetOrCreateSimulator(); |
| 89 | + if (simulator == null) |
| 90 | + return new ErrorResponse("Failed to create InputSimulator in the scene."); |
| 91 | + |
| 92 | + simulator.QueueMouseClick(x, y, button); |
| 93 | + |
| 94 | + return new SuccessResponse( |
| 95 | + $"Queued mouse click at ({x}, {y}) button={button}.", |
| 96 | + new { x, y, button }); |
| 97 | + } |
| 98 | + |
| 99 | + private static object HandleSendMouseMove(ToolParams p) |
| 100 | + { |
| 101 | + var guard = RequirePlayMode(); |
| 102 | + if (guard != null) return guard; |
| 103 | + |
| 104 | + float deltaX = (float)(p.GetInt("deltaX") ?? 0); |
| 105 | + float deltaY = (float)(p.GetInt("deltaY") ?? 0); |
| 106 | + |
| 107 | + var simulator = GetOrCreateSimulator(); |
| 108 | + if (simulator == null) |
| 109 | + return new ErrorResponse("Failed to create InputSimulator in the scene."); |
| 110 | + |
| 111 | + simulator.QueueMouseMove(deltaX, deltaY); |
| 112 | + |
| 113 | + return new SuccessResponse( |
| 114 | + $"Queued mouse move delta ({deltaX}, {deltaY}).", |
| 115 | + new { deltaX, deltaY }); |
| 116 | + } |
| 117 | + |
| 118 | + private static object HandleSendSequence(ToolParams p, JObject raw) |
| 119 | + { |
| 120 | + var guard = RequirePlayMode(); |
| 121 | + if (guard != null) return guard; |
| 122 | + |
| 123 | + var stepsToken = raw["steps"]; |
| 124 | + if (stepsToken == null || stepsToken.Type != JTokenType.Array) |
| 125 | + return new ErrorResponse("'steps' parameter is required and must be an array of {action, params, duration_ms} objects."); |
| 126 | + |
| 127 | + var simulator = GetOrCreateSimulator(); |
| 128 | + if (simulator == null) |
| 129 | + return new ErrorResponse("Failed to create InputSimulator in the scene."); |
| 130 | + |
| 131 | + var steps = (JArray)stepsToken; |
| 132 | + int queued = 0; |
| 133 | + |
| 134 | + foreach (var step in steps) |
| 135 | + { |
| 136 | + if (step.Type != JTokenType.Object) continue; |
| 137 | + var stepObj = (JObject)step; |
| 138 | + string stepAction = stepObj["action"]?.ToString()?.ToLowerInvariant() ?? ""; |
| 139 | + int durationMs = ParamCoercion.CoerceIntNullable(stepObj["duration_ms"] ?? stepObj["durationMs"]) ?? 100; |
| 140 | + |
| 141 | + switch (stepAction) |
| 142 | + { |
| 143 | + case "key": |
| 144 | + string key = stepObj["key"]?.ToString() ?? ""; |
| 145 | + if (!string.IsNullOrEmpty(key)) |
| 146 | + { |
| 147 | + simulator.QueueKeyAction(key, durationMs, true, true); |
| 148 | + queued++; |
| 149 | + } |
| 150 | + break; |
| 151 | + case "mouse_click": |
| 152 | + float cx = (float)(ParamCoercion.CoerceIntNullable(stepObj["x"]) ?? 0); |
| 153 | + float cy = (float)(ParamCoercion.CoerceIntNullable(stepObj["y"]) ?? 0); |
| 154 | + int btn = ParamCoercion.CoerceIntNullable(stepObj["button"]) ?? 0; |
| 155 | + simulator.QueueMouseClick(cx, cy, btn); |
| 156 | + queued++; |
| 157 | + break; |
| 158 | + case "mouse_move": |
| 159 | + float dx = (float)(ParamCoercion.CoerceIntNullable(stepObj["deltaX"] ?? stepObj["delta_x"]) ?? 0); |
| 160 | + float dy = (float)(ParamCoercion.CoerceIntNullable(stepObj["deltaY"] ?? stepObj["delta_y"]) ?? 0); |
| 161 | + simulator.QueueMouseMove(dx, dy); |
| 162 | + queued++; |
| 163 | + break; |
| 164 | + case "wait": |
| 165 | + simulator.QueueWait(durationMs); |
| 166 | + queued++; |
| 167 | + break; |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + return new SuccessResponse( |
| 172 | + $"Queued {queued} input steps.", |
| 173 | + new { queued, totalSteps = steps.Count }); |
| 174 | + } |
| 175 | + |
| 176 | + private static object HandleGetState(ToolParams p) |
| 177 | + { |
| 178 | + var guard = RequirePlayMode(); |
| 179 | + if (guard != null) return guard; |
| 180 | + |
| 181 | + string query = p.Get("query") ?? "default"; |
| 182 | + var state = new Dictionary<string, object>(); |
| 183 | + |
| 184 | + // Always include basic state |
| 185 | + state["isPlaying"] = EditorApplication.isPlaying; |
| 186 | + state["isPaused"] = EditorApplication.isPaused; |
| 187 | + state["time"] = Time.time; |
| 188 | + state["frameCount"] = Time.frameCount; |
| 189 | + |
| 190 | + // Find player-like objects |
| 191 | + var cameras = UnityEngine.Object.FindObjectsOfType<Camera>(); |
| 192 | + if (cameras.Length > 0) |
| 193 | + { |
| 194 | + var mainCam = Camera.main ?? cameras[0]; |
| 195 | + state["cameraPosition"] = new[] { mainCam.transform.position.x, mainCam.transform.position.y, mainCam.transform.position.z }; |
| 196 | + state["cameraRotation"] = new[] { mainCam.transform.eulerAngles.x, mainCam.transform.eulerAngles.y, mainCam.transform.eulerAngles.z }; |
| 197 | + } |
| 198 | + |
| 199 | + // Look for common player patterns |
| 200 | + var player = GameObject.FindWithTag("Player"); |
| 201 | + if (player != null) |
| 202 | + { |
| 203 | + state["playerPosition"] = new[] { player.transform.position.x, player.transform.position.y, player.transform.position.z }; |
| 204 | + state["playerRotation"] = new[] { player.transform.eulerAngles.x, player.transform.eulerAngles.y, player.transform.eulerAngles.z }; |
| 205 | + |
| 206 | + // Check for Rigidbody velocity |
| 207 | + var rb = player.GetComponent<Rigidbody>(); |
| 208 | + if (rb != null) |
| 209 | + { |
| 210 | +#if UNITY_2023_3_OR_NEWER |
| 211 | + state["playerVelocity"] = new[] { rb.linearVelocity.x, rb.linearVelocity.y, rb.linearVelocity.z }; |
| 212 | + state["playerSpeed"] = rb.linearVelocity.magnitude; |
| 213 | +#else |
| 214 | + state["playerVelocity"] = new[] { rb.velocity.x, rb.velocity.y, rb.velocity.z }; |
| 215 | + state["playerSpeed"] = rb.velocity.magnitude; |
| 216 | +#endif |
| 217 | + } |
| 218 | + |
| 219 | + var rb2d = player.GetComponent<Rigidbody2D>(); |
| 220 | + if (rb2d != null) |
| 221 | + { |
| 222 | +#if UNITY_2023_3_OR_NEWER |
| 223 | + state["playerVelocity"] = new[] { rb2d.linearVelocity.x, rb2d.linearVelocity.y }; |
| 224 | + state["playerSpeed"] = rb2d.linearVelocity.magnitude; |
| 225 | +#else |
| 226 | + state["playerVelocity"] = new[] { rb2d.velocity.x, rb2d.velocity.y }; |
| 227 | + state["playerSpeed"] = rb2d.velocity.magnitude; |
| 228 | +#endif |
| 229 | + } |
| 230 | + } |
| 231 | + |
| 232 | + // Active UI elements (for click-based games) |
| 233 | + if (query == "ui" || query == "default") |
| 234 | + { |
| 235 | + var canvases = UnityEngine.Object.FindObjectsOfType<Canvas>(); |
| 236 | + var activeUiElements = new List<object>(); |
| 237 | + foreach (var canvas in canvases) |
| 238 | + { |
| 239 | + if (!canvas.gameObject.activeInHierarchy) continue; |
| 240 | + var buttons = canvas.GetComponentsInChildren<UnityEngine.UI.Button>(false); |
| 241 | + foreach (var btn in buttons) |
| 242 | + { |
| 243 | + if (!btn.gameObject.activeInHierarchy || !btn.interactable) continue; |
| 244 | + var rt = btn.GetComponent<RectTransform>(); |
| 245 | + activeUiElements.Add(new Dictionary<string, object> |
| 246 | + { |
| 247 | + { "name", btn.gameObject.name }, |
| 248 | + { "type", "Button" }, |
| 249 | + { "interactable", btn.interactable }, |
| 250 | + { "position", rt != null ? new[] { rt.position.x, rt.position.y } : null }, |
| 251 | + }); |
| 252 | + } |
| 253 | + } |
| 254 | + if (activeUiElements.Count > 0) |
| 255 | + state["activeUiElements"] = activeUiElements; |
| 256 | + } |
| 257 | + |
| 258 | + return new SuccessResponse("Game state snapshot.", state); |
| 259 | + } |
| 260 | + |
| 261 | + private static MCPInputSimulator GetOrCreateSimulator() |
| 262 | + { |
| 263 | + var existing = UnityEngine.Object.FindObjectsOfType<MCPInputSimulator>(); |
| 264 | + if (existing.Length > 0) return existing[0]; |
| 265 | + var go = new GameObject("__MCP_InputSimulator__"); |
| 266 | + go.hideFlags = HideFlags.HideAndDontSave; |
| 267 | + return go.AddComponent<MCPInputSimulator>(); |
| 268 | + } |
| 269 | + } |
| 270 | +} |
0 commit comments