From 0b51ff50d57c7a92ca032f57ffde6722f45ca1cf Mon Sep 17 00:00:00 2001 From: Justin Barnett Date: Mon, 31 Mar 2025 16:34:24 -0400 Subject: [PATCH] remove all optional and union and any parameters for cursor --- Editor/Tools/ManageGameObject.cs | 382 +++++++++++++++++++++++++++--- Editor/Tools/ManageScript.cs | 52 +++- Python/tools/execute_menu_item.py | 20 +- Python/tools/manage_asset.py | 20 +- Python/tools/manage_editor.py | 22 +- Python/tools/manage_gameobject.py | 67 +++--- Python/tools/manage_scene.py | 17 +- Python/tools/manage_script.py | 48 ++-- Python/tools/read_console.py | 36 +-- Python/unity_connection.py | 23 +- climber-prompt.md | 66 ++++++ climber-prompt.md.meta | 7 + 12 files changed, 608 insertions(+), 152 deletions(-) create mode 100644 climber-prompt.md create mode 100644 climber-prompt.md.meta diff --git a/Editor/Tools/ManageGameObject.cs b/Editor/Tools/ManageGameObject.cs index d85c09b..9c2e7a7 100644 --- a/Editor/Tools/ManageGameObject.cs +++ b/Editor/Tools/ManageGameObject.cs @@ -30,7 +30,12 @@ namespace UnityMCP.Editor.Tools // Parameters used by various actions JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID) string searchMethod = @params["searchMethod"]?.ToString().ToLower(); + + // Get common parameters (consolidated) string name = @params["name"]?.ToString(); + string tag = @params["tag"]?.ToString(); + string layer = @params["layer"]?.ToString(); + JToken parentToken = @params["parent"]; // --- Prefab Redirection Check --- string targetPath = targetToken?.Type == JTokenType.String ? targetToken.ToString() : null; @@ -308,6 +313,21 @@ namespace UnityMCP.Editor.Tools } } } + + // Set Layer (new for create action) + string layerName = @params["layer"]?.ToString(); + if (!string.IsNullOrEmpty(layerName)) + { + int layerId = LayerMask.NameToLayer(layerName); + if (layerId != -1) + { + newGo.layer = layerId; + } + else + { + Debug.LogWarning($"[ManageGameObject.Create] Layer '{layerName}' not found. Using default layer."); + } + } // Add Components if (@params["componentsToAdd"] is JArray componentsToAddArray) @@ -438,22 +458,22 @@ namespace UnityMCP.Editor.Tools bool modified = false; - // Rename - string newName = @params["newName"]?.ToString(); - if (!string.IsNullOrEmpty(newName) && targetGo.name != newName) + // Rename (using consolidated 'name' parameter) + string name = @params["name"]?.ToString(); + if (!string.IsNullOrEmpty(name) && targetGo.name != name) { - targetGo.name = newName; - modified = true; + targetGo.name = name; + modified = true; } - // Change Parent - JToken newParentToken = @params["newParent"]; - if (newParentToken != null) + // Change Parent (using consolidated 'parent' parameter) + JToken parentToken = @params["parent"]; + if (parentToken != null) { - GameObject newParentGo = FindObjectInternal(newParentToken, "by_id_or_name_or_path"); - if (newParentGo == null && !(newParentToken.Type == JTokenType.Null || (newParentToken.Type == JTokenType.String && string.IsNullOrEmpty(newParentToken.ToString())))) + GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path"); + if (newParentGo == null && !(parentToken.Type == JTokenType.Null || (parentToken.Type == JTokenType.String && string.IsNullOrEmpty(parentToken.ToString())))) { - return Response.Error($"New parent ('{newParentToken}') not found."); + return Response.Error($"New parent ('{parentToken}') not found."); } // Check for hierarchy loops if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform)) @@ -475,14 +495,14 @@ namespace UnityMCP.Editor.Tools modified = true; } - // Change Tag - string newTag = @params["newTag"]?.ToString(); + // Change Tag (using consolidated 'tag' parameter) + string tag = @params["tag"]?.ToString(); // Only attempt to change tag if a non-null tag is provided and it's different from the current one. // Allow setting an empty string to remove the tag (Unity uses "Untagged"). - if (newTag != null && targetGo.tag != newTag) + if (tag != null && targetGo.tag != tag) { // Ensure the tag is not empty, if empty, it means "Untagged" implicitly - string tagToSet = string.IsNullOrEmpty(newTag) ? "Untagged" : newTag; + string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; try { // First attempt to set the tag @@ -522,28 +542,19 @@ namespace UnityMCP.Editor.Tools } } - // Change Layer - JToken newLayerToken = @params["newLayer"]; - if (newLayerToken != null) + // Change Layer (using consolidated 'layer' parameter) + string layerName = @params["layer"]?.ToString(); + if (!string.IsNullOrEmpty(layerName)) { - int layer = -1; - if (newLayerToken.Type == JTokenType.Integer) + int layerId = LayerMask.NameToLayer(layerName); + if (layerId == -1 && layerName != "Default") { - layer = newLayerToken.ToObject(); + return Response.Error($"Invalid layer specified: '{layerName}'. Use a valid layer name."); } - else if (newLayerToken.Type == JTokenType.String) + if (layerId != -1 && targetGo.layer != layerId) { - layer = LayerMask.NameToLayer(newLayerToken.ToString()); - } - - if (layer == -1 && newLayerToken.ToString() != "Default") // LayerMask.NameToLayer returns -1 for invalid names - { - return Response.Error($"Invalid layer specified: '{newLayerToken}'. Use a valid layer name or index."); - } - if (layer != -1 && targetGo.layer != layer) - { - targetGo.layer = layer; - modified = true; + targetGo.layer = layerId; + modified = true; } } @@ -999,6 +1010,13 @@ namespace UnityMCP.Editor.Tools return Response.Error($"Failed to add component '{typeName}' to '{targetGo.name}'. It might be disallowed (e.g., adding script twice)."); } + // Set default values for specific component types + if (newComponent is Light light) + { + // Default newly added lights to directional + light.type = LightType.Directional; + } + // Set properties if provided if (properties != null) { @@ -1104,6 +1122,13 @@ namespace UnityMCP.Editor.Tools try { + // Handle special case for materials with dot notation (material.property) + // Examples: material.color, sharedMaterial.color, materials[0].color + if (memberName.Contains('.') || memberName.Contains('[')) + { + return SetNestedProperty(target, memberName, value); + } + PropertyInfo propInfo = type.GetProperty(memberName, flags); if (propInfo != null && propInfo.CanWrite) { @@ -1134,6 +1159,265 @@ namespace UnityMCP.Editor.Tools return false; } + /// + /// Sets a nested property using dot notation (e.g., "material.color") or array access (e.g., "materials[0]") + /// + private static bool SetNestedProperty(object target, string path, JToken value) + { + try + { + // Split the path into parts (handling both dot notation and array indexing) + string[] pathParts = SplitPropertyPath(path); + if (pathParts.Length == 0) return false; + + object currentObject = target; + Type currentType = currentObject.GetType(); + BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + + // Traverse the path until we reach the final property + for (int i = 0; i < pathParts.Length - 1; i++) + { + string part = pathParts[i]; + bool isArray = false; + int arrayIndex = -1; + + // Check if this part contains array indexing + if (part.Contains("[")) + { + int startBracket = part.IndexOf('['); + int endBracket = part.IndexOf(']'); + if (startBracket > 0 && endBracket > startBracket) + { + string indexStr = part.Substring(startBracket + 1, endBracket - startBracket - 1); + if (int.TryParse(indexStr, out arrayIndex)) + { + isArray = true; + part = part.Substring(0, startBracket); + } + } + } + + // Get the property/field + PropertyInfo propInfo = currentType.GetProperty(part, flags); + FieldInfo fieldInfo = null; + if (propInfo == null) + { + fieldInfo = currentType.GetField(part, flags); + if (fieldInfo == null) + { + Debug.LogWarning($"[SetNestedProperty] Could not find property or field '{part}' on type '{currentType.Name}'"); + return false; + } + } + + // Get the value + currentObject = propInfo != null ? propInfo.GetValue(currentObject) : fieldInfo.GetValue(currentObject); + + // If the current property is null, we need to stop + if (currentObject == null) + { + Debug.LogWarning($"[SetNestedProperty] Property '{part}' is null, cannot access nested properties."); + return false; + } + + // If this is an array/list access, get the element at the index + if (isArray) + { + if (currentObject is Material[]) + { + var materials = currentObject as Material[]; + if (arrayIndex < 0 || arrayIndex >= materials.Length) + { + Debug.LogWarning($"[SetNestedProperty] Material index {arrayIndex} out of range (0-{materials.Length-1})"); + return false; + } + currentObject = materials[arrayIndex]; + } + else if (currentObject is System.Collections.IList) + { + var list = currentObject as System.Collections.IList; + if (arrayIndex < 0 || arrayIndex >= list.Count) + { + Debug.LogWarning($"[SetNestedProperty] Index {arrayIndex} out of range (0-{list.Count-1})"); + return false; + } + currentObject = list[arrayIndex]; + } + else + { + Debug.LogWarning($"[SetNestedProperty] Property '{part}' is not an array or list, cannot access by index."); + return false; + } + } + + // Update type for next iteration + currentType = currentObject.GetType(); + } + + // Set the final property + string finalPart = pathParts[pathParts.Length - 1]; + + // Special handling for Material properties (shader properties) + if (currentObject is Material material && finalPart.StartsWith("_")) + { + // Handle various material property types + if (value is JArray jArray) + { + if (jArray.Count == 4) // Color with alpha + { + Color color = new Color( + jArray[0].ToObject(), + jArray[1].ToObject(), + jArray[2].ToObject(), + jArray[3].ToObject() + ); + material.SetColor(finalPart, color); + return true; + } + else if (jArray.Count == 3) // Color without alpha + { + Color color = new Color( + jArray[0].ToObject(), + jArray[1].ToObject(), + jArray[2].ToObject(), + 1.0f + ); + material.SetColor(finalPart, color); + return true; + } + else if (jArray.Count == 2) // Vector2 + { + Vector2 vec = new Vector2( + jArray[0].ToObject(), + jArray[1].ToObject() + ); + material.SetVector(finalPart, vec); + return true; + } + else if (jArray.Count == 4) // Vector4 + { + Vector4 vec = new Vector4( + jArray[0].ToObject(), + jArray[1].ToObject(), + jArray[2].ToObject(), + jArray[3].ToObject() + ); + material.SetVector(finalPart, vec); + return true; + } + } + else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer) + { + material.SetFloat(finalPart, value.ToObject()); + return true; + } + else if (value.Type == JTokenType.Boolean) + { + material.SetFloat(finalPart, value.ToObject() ? 1f : 0f); + return true; + } + else if (value.Type == JTokenType.String) + { + // Might be a texture path + string texturePath = value.ToString(); + if (texturePath.EndsWith(".png") || texturePath.EndsWith(".jpg") || texturePath.EndsWith(".tga")) + { + Texture2D texture = AssetDatabase.LoadAssetAtPath(texturePath); + if (texture != null) + { + material.SetTexture(finalPart, texture); + return true; + } + } + else + { + // Materials don't have SetString, use SetTextureOffset as workaround or skip + // material.SetString(finalPart, texturePath); + Debug.LogWarning($"[SetNestedProperty] String values not directly supported for material property {finalPart}"); + return false; + } + } + + Debug.LogWarning($"[SetNestedProperty] Unsupported material property value type: {value.Type} for {finalPart}"); + return false; + } + + // For standard properties (not shader specific) + PropertyInfo finalPropInfo = currentType.GetProperty(finalPart, flags); + if (finalPropInfo != null && finalPropInfo.CanWrite) + { + object convertedValue = ConvertJTokenToType(value, finalPropInfo.PropertyType); + if (convertedValue != null) + { + finalPropInfo.SetValue(currentObject, convertedValue); + return true; + } + } + else + { + FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags); + if (finalFieldInfo != null) + { + object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType); + if (convertedValue != null) + { + finalFieldInfo.SetValue(currentObject, convertedValue); + return true; + } + } + else + { + Debug.LogWarning($"[SetNestedProperty] Could not find final property or field '{finalPart}' on type '{currentType.Name}'"); + } + } + } + catch (Exception ex) + { + Debug.LogError($"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}"); + } + + return false; + } + + /// + /// Split a property path into parts, handling both dot notation and array indexers + /// + private static string[] SplitPropertyPath(string path) + { + // Handle complex paths with both dots and array indexers + List parts = new List(); + int startIndex = 0; + bool inBrackets = false; + + for (int i = 0; i < path.Length; i++) + { + char c = path[i]; + + if (c == '[') + { + inBrackets = true; + } + else if (c == ']') + { + inBrackets = false; + } + else if (c == '.' && !inBrackets) + { + // Found a dot separator outside of brackets + parts.Add(path.Substring(startIndex, i - startIndex)); + startIndex = i + 1; + } + } + + // Add the final part + if (startIndex < path.Length) + { + parts.Add(path.Substring(startIndex)); + } + + return parts.ToArray(); + } + /// /// Simple JToken to Type conversion for common Unity types. /// @@ -1141,6 +1425,38 @@ namespace UnityMCP.Editor.Tools { try { + // Unwrap nested material properties if we're assigning to a Material + if (typeof(Material).IsAssignableFrom(targetType) && token is JObject materialProps) + { + // Handle case where we're passing shader properties directly in a nested object + string materialPath = token["path"]?.ToString(); + if (!string.IsNullOrEmpty(materialPath)) + { + // Load the material by path + Material material = AssetDatabase.LoadAssetAtPath(materialPath); + if (material != null) + { + // If there are additional properties, set them + foreach (var prop in materialProps.Properties()) + { + if (prop.Name != "path") + { + SetProperty(material, prop.Name, prop.Value); + } + } + return material; + } + else + { + Debug.LogWarning($"[ConvertJTokenToType] Could not load material at path: '{materialPath}'"); + return null; + } + } + + // If no path is specified, could be a dynamic material or instance set by reference + return null; + } + // Basic types first if (targetType == typeof(string)) return token.ToObject(); if (targetType == typeof(int)) return token.ToObject(); diff --git a/Editor/Tools/ManageScript.cs b/Editor/Tools/ManageScript.cs index ef73210..f041d1c 100644 --- a/Editor/Tools/ManageScript.cs +++ b/Editor/Tools/ManageScript.cs @@ -23,7 +23,26 @@ namespace UnityMCP.Editor.Tools string action = @params["action"]?.ToString().ToLower(); string name = @params["name"]?.ToString(); string path = @params["path"]?.ToString(); // Relative to Assets/ - string contents = @params["contents"]?.ToString(); + string contents = null; + + // Check if we have base64 encoded contents + bool contentsEncoded = @params["contentsEncoded"]?.ToObject() ?? false; + if (contentsEncoded && @params["encodedContents"] != null) + { + try + { + contents = DecodeBase64(@params["encodedContents"].ToString()); + } + catch (Exception e) + { + return Response.Error($"Failed to decode script contents: {e.Message}"); + } + } + else + { + contents = @params["contents"]?.ToString(); + } + string scriptType = @params["scriptType"]?.ToString(); // For templates/validation string namespaceName = @params["namespace"]?.ToString(); // For organizing code @@ -93,6 +112,24 @@ namespace UnityMCP.Editor.Tools } } + /// + /// Decode base64 string to normal text + /// + private static string DecodeBase64(string encoded) + { + byte[] data = Convert.FromBase64String(encoded); + return System.Text.Encoding.UTF8.GetString(data); + } + + /// + /// Encode text to base64 string + /// + private static string EncodeBase64(string text) + { + byte[] data = System.Text.Encoding.UTF8.GetBytes(text); + return Convert.ToBase64String(data); + } + private static object CreateScript(string fullPath, string relativePath, string name, string contents, string scriptType, string namespaceName) { // Check if script already exists @@ -138,7 +175,18 @@ namespace UnityMCP.Editor.Tools try { string contents = File.ReadAllText(fullPath); - return Response.Success($"Script '{Path.GetFileName(relativePath)}' read successfully.", new { path = relativePath, contents = contents }); + + // Return both normal and encoded contents for larger files + bool isLarge = contents.Length > 10000; // If content is large, include encoded version + var responseData = new { + path = relativePath, + contents = contents, + // For large files, also include base64-encoded version + encodedContents = isLarge ? EncodeBase64(contents) : null, + contentsEncoded = isLarge + }; + + return Response.Success($"Script '{Path.GetFileName(relativePath)}' read successfully.", responseData); } catch (Exception e) { diff --git a/Python/tools/execute_menu_item.py b/Python/tools/execute_menu_item.py index daa45b1..a4ebc67 100644 --- a/Python/tools/execute_menu_item.py +++ b/Python/tools/execute_menu_item.py @@ -1,8 +1,9 @@ """ Defines the execute_menu_item tool for running Unity Editor menu commands. """ -from typing import Optional, Dict, Any +from typing import Dict, Any from mcp.server.fastmcp import FastMCP, Context +from unity_connection import get_unity_connection # Import unity_connection module def register_execute_menu_item_tools(mcp: FastMCP): """Registers the execute_menu_item tool with the MCP server.""" @@ -11,8 +12,8 @@ def register_execute_menu_item_tools(mcp: FastMCP): async def execute_menu_item( ctx: Context, menu_path: str, - action: Optional[str] = 'execute', - parameters: Optional[Dict[str, Any]] = None, + action: str = 'execute', + parameters: Dict[str, Any] = None, ) -> Dict[str, Any]: """Executes a Unity Editor menu item via its path (e.g., "File/Save Project"). @@ -41,9 +42,10 @@ def register_execute_menu_item_tools(mcp: FastMCP): if "parameters" not in params_dict: params_dict["parameters"] = {} # Ensure parameters dict exists - # Forward the command to the Unity editor handler - # The C# handler is the static method HandleCommand in the ExecuteMenuItem class. - # We assume ctx.call is the correct way to invoke it via FastMCP. - # Note: The exact target string might need adjustment based on FastMCP's specifics. - csharp_handler_target = "UnityMCP.Editor.Tools.ExecuteMenuItem.HandleCommand" - return await ctx.call(csharp_handler_target, params_dict) \ No newline at end of file + # Get Unity connection and send the command + # We use the unity_connection module to communicate with Unity + unity_conn = get_unity_connection() + + # Send command to the ExecuteMenuItem C# handler + # The command type should match what the Unity side expects + return unity_conn.send_command("execute_menu_item", params_dict) \ No newline at end of file diff --git a/Python/tools/manage_asset.py b/Python/tools/manage_asset.py index 492a460..328b85a 100644 --- a/Python/tools/manage_asset.py +++ b/Python/tools/manage_asset.py @@ -2,7 +2,7 @@ Defines the manage_asset tool for interacting with Unity assets. """ import asyncio # Added: Import asyncio for running sync code in async -from typing import Optional, Dict, Any, List +from typing import Dict, Any from mcp.server.fastmcp import FastMCP, Context # from ..unity_connection import get_unity_connection # Original line that caused error from unity_connection import get_unity_connection # Use absolute import relative to Python dir @@ -15,15 +15,15 @@ def register_manage_asset_tools(mcp: FastMCP): ctx: Context, action: str, path: str, - asset_type: Optional[str] = None, - properties: Optional[Dict[str, Any]] = None, - destination: Optional[str] = None, - generate_preview: Optional[bool] = False, - search_pattern: Optional[str] = None, - filter_type: Optional[str] = None, - filter_date_after: Optional[str] = None, - page_size: Optional[int] = None, - page_number: Optional[int] = None + asset_type: str = None, + properties: Dict[str, Any] = None, + destination: str = None, + generate_preview: bool = False, + search_pattern: str = None, + filter_type: str = None, + filter_date_after: str = None, + page_size: int = None, + page_number: int = None ) -> Dict[str, Any]: """Performs asset operations (import, create, modify, delete, etc.) in Unity. diff --git a/Python/tools/manage_editor.py b/Python/tools/manage_editor.py index 4ba65c1..b256e6c 100644 --- a/Python/tools/manage_editor.py +++ b/Python/tools/manage_editor.py @@ -1,5 +1,5 @@ from mcp.server.fastmcp import FastMCP, Context -from typing import Optional, Dict, Any, Union +from typing import Dict, Any from unity_connection import get_unity_connection def register_manage_editor_tools(mcp: FastMCP): @@ -9,11 +9,11 @@ def register_manage_editor_tools(mcp: FastMCP): def manage_editor( ctx: Context, action: str, - wait_for_completion: Optional[bool] = None, + wait_for_completion: bool = None, # --- Parameters for specific actions --- - tool_name: Optional[str] = None, - tag_name: Optional[str] = None, - layer_name: Optional[str] = None, + tool_name: str = None, + tag_name: str = None, + layer_name: str = None, ) -> Dict[str, Any]: """Controls and queries the Unity editor's state and settings. @@ -50,14 +50,4 @@ def register_manage_editor_tools(mcp: FastMCP): return {"success": False, "message": response.get("error", "An unknown error occurred during editor management.")} except Exception as e: - return {"success": False, "message": f"Python error managing editor: {str(e)}"} - - # Example of potentially splitting into more specific tools: - # @mcp.tool() - # def get_editor_state(ctx: Context) -> Dict[str, Any]: ... - # @mcp.tool() - # def set_editor_playmode(ctx: Context, state: str) -> Dict[str, Any]: ... # state='play'/'pause'/'stop' - # @mcp.tool() - # def add_editor_tag(ctx: Context, tag_name: str) -> Dict[str, Any]: ... - # @mcp.tool() - # def add_editor_layer(ctx: Context, layer_name: str) -> Dict[str, Any]: ... \ No newline at end of file + return {"success": False, "message": f"Python error managing editor: {str(e)}"} \ No newline at end of file diff --git a/Python/tools/manage_gameobject.py b/Python/tools/manage_gameobject.py index 931f6d6..a65331f 100644 --- a/Python/tools/manage_gameobject.py +++ b/Python/tools/manage_gameobject.py @@ -1,5 +1,5 @@ from mcp.server.fastmcp import FastMCP, Context -from typing import Optional, Dict, Any, List, Union +from typing import Dict, Any, List from unity_connection import get_unity_connection def register_manage_gameobject_tools(mcp: FastMCP): @@ -9,42 +9,43 @@ def register_manage_gameobject_tools(mcp: FastMCP): def manage_gameobject( ctx: Context, action: str, - target: Optional[Union[str, int]] = None, - search_method: Optional[str] = None, - # --- Parameters for 'create' --- - name: Optional[str] = None, - tag: Optional[str] = None, - parent: Optional[Union[str, int]] = None, - position: Optional[List[float]] = None, - rotation: Optional[List[float]] = None, - scale: Optional[List[float]] = None, - components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None, - primitive_type: Optional[str] = None, - save_as_prefab: Optional[bool] = False, - prefab_path: Optional[str] = None, - prefab_folder: Optional[str] = "Assets/Prefabs", + target: str = None, # GameObject identifier by name or path + search_method: str = None, + # --- Combined Parameters for Create/Modify --- + name: str = None, # Used for both 'create' (new object name) and 'modify' (rename) + tag: str = None, # Used for both 'create' (initial tag) and 'modify' (change tag) + parent: str = None, # Used for both 'create' (initial parent) and 'modify' (change parent) + position: List[float] = None, + rotation: List[float] = None, + scale: List[float] = None, + components_to_add: List[str] = None, # List of component names to add + primitive_type: str = None, + save_as_prefab: bool = False, + prefab_path: str = None, + prefab_folder: str = "Assets/Prefabs", # --- Parameters for 'modify' --- - new_name: Optional[str] = None, - new_parent: Optional[Union[str, int]] = None, - set_active: Optional[bool] = None, - new_tag: Optional[str] = None, - new_layer: Optional[Union[str, int]] = None, - components_to_remove: Optional[List[str]] = None, - component_properties: Optional[Dict[str, Dict[str, Any]]] = None, + set_active: bool = None, + layer: str = None, # Layer name + components_to_remove: List[str] = None, + component_properties: Dict[str, Dict[str, Any]] = None, # --- Parameters for 'find' --- - search_term: Optional[str] = None, - find_all: Optional[bool] = False, - search_in_children: Optional[bool] = False, - search_inactive: Optional[bool] = False, + search_term: str = None, + find_all: bool = False, + search_in_children: bool = False, + search_inactive: bool = False, # -- Component Management Arguments -- - component_name: Optional[str] = None, + component_name: str = None, ) -> Dict[str, Any]: """Manages GameObjects: create, modify, delete, find, and component operations. Args: action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property'). - target: GameObject identifier (name, path, ID) for modify/delete/component actions. + target: GameObject identifier (name or path string) for modify/delete/component actions. search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups. + name: GameObject name - used for both 'create' (initial name) and 'modify' (rename). + tag: Tag name - used for both 'create' (initial tag) and 'modify' (change tag). + parent: Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent). + layer: Layer name - used for both 'create' (initial layer) and 'modify' (change layer). component_properties: Dict mapping Component names to their properties to set. Example: {"Rigidbody": {"mass": 10.0, "useGravity": True}}, To set references: @@ -52,7 +53,10 @@ def register_manage_gameobject_tools(mcp: FastMCP): - Use a dict for scene objects/components, e.g.: {"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}} (assigns GameObject) {"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}} (assigns Component) - Action-specific arguments (e.g., name, parent, position for 'create'; + Example set nested property: + - Access shared material: {"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}} + components_to_add: List of component names to add. + Action-specific arguments (e.g., position, rotation, scale for create/modify; component_name for component actions; search_term, find_all for 'find'). @@ -79,11 +83,8 @@ def register_manage_gameobject_tools(mcp: FastMCP): "saveAsPrefab": save_as_prefab, "prefabPath": prefab_path, "prefabFolder": prefab_folder, - "newName": new_name, - "newParent": new_parent, "setActive": set_active, - "newTag": new_tag, - "newLayer": new_layer, + "layer": layer, "componentsToRemove": components_to_remove, "componentProperties": component_properties, "searchTerm": search_term, diff --git a/Python/tools/manage_scene.py b/Python/tools/manage_scene.py index 79e02d2..44981f6 100644 --- a/Python/tools/manage_scene.py +++ b/Python/tools/manage_scene.py @@ -1,5 +1,5 @@ from mcp.server.fastmcp import FastMCP, Context -from typing import Optional, Dict, Any +from typing import Dict, Any from unity_connection import get_unity_connection def register_manage_scene_tools(mcp: FastMCP): @@ -9,9 +9,9 @@ def register_manage_scene_tools(mcp: FastMCP): def manage_scene( ctx: Context, action: str, - name: Optional[str] = None, - path: Optional[str] = None, - build_index: Optional[int] = None, + name: str, + path: str, + build_index: int, ) -> Dict[str, Any]: """Manages Unity scenes (load, save, create, get hierarchy, etc.). @@ -26,7 +26,6 @@ def register_manage_scene_tools(mcp: FastMCP): Dictionary with results ('success', 'message', 'data'). """ try: - # Prepare parameters, removing None values params = { "action": action, "name": name, @@ -45,10 +44,4 @@ def register_manage_scene_tools(mcp: FastMCP): return {"success": False, "message": response.get("error", "An unknown error occurred during scene management.")} except Exception as e: - return {"success": False, "message": f"Python error managing scene: {str(e)}"} - - # Consider adding specific tools if the single 'manage_scene' becomes too complex: - # @mcp.tool() - # def load_scene(ctx: Context, name: str, path: Optional[str] = None, build_index: Optional[int] = None) -> Dict[str, Any]: ... - # @mcp.tool() - # def get_scene_hierarchy(ctx: Context) -> Dict[str, Any]: ... \ No newline at end of file + return {"success": False, "message": f"Python error managing scene: {str(e)}"} \ No newline at end of file diff --git a/Python/tools/manage_script.py b/Python/tools/manage_script.py index c6f2744..22e0953 100644 --- a/Python/tools/manage_script.py +++ b/Python/tools/manage_script.py @@ -1,7 +1,8 @@ from mcp.server.fastmcp import FastMCP, Context -from typing import Optional, Dict, Any +from typing import Dict, Any from unity_connection import get_unity_connection import os +import base64 def register_manage_script_tools(mcp: FastMCP): """Register all script management tools with the MCP server.""" @@ -11,10 +12,10 @@ def register_manage_script_tools(mcp: FastMCP): ctx: Context, action: str, name: str, - path: Optional[str] = None, - contents: Optional[str] = None, - script_type: Optional[str] = None, - namespace: Optional[str] = None + path: str, + contents: str, + script_type: str, + namespace: str ) -> Dict[str, Any]: """Manages C# scripts in Unity (create, read, update, delete). Make reference variables public for easier access in the Unity Editor. @@ -22,10 +23,10 @@ def register_manage_script_tools(mcp: FastMCP): Args: action: Operation ('create', 'read', 'update', 'delete'). name: Script name (no .cs extension). - path: Asset path (optional, default: "Assets/"). + path: Asset path (default: "Assets/"). contents: C# code for 'create'/'update'. - script_type: Type hint (e.g., 'MonoBehaviour', optional). - namespace: Script namespace (optional). + script_type: Type hint (e.g., 'MonoBehaviour'). + namespace: Script namespace. Returns: Dictionary with results ('success', 'message', 'data'). @@ -36,10 +37,19 @@ def register_manage_script_tools(mcp: FastMCP): "action": action, "name": name, "path": path, - "contents": contents, - "scriptType": script_type, - "namespace": namespace + "namespace": namespace, + "scriptType": script_type } + + # Base64 encode the contents if they exist to avoid JSON escaping issues + if contents is not None: + if action in ['create', 'update']: + # Encode content for safer transmission + params["encodedContents"] = base64.b64encode(contents.encode('utf-8')).decode('utf-8') + params["contentsEncoded"] = True + else: + params["contents"] = contents + # Remove None values so they don't get sent as null params = {k: v for k, v in params.items() if v is not None} @@ -48,17 +58,17 @@ def register_manage_script_tools(mcp: FastMCP): # Process response from Unity if response.get("success"): + # If the response contains base64 encoded content, decode it + if response.get("data", {}).get("contentsEncoded"): + decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8') + response["data"]["contents"] = decoded_contents + del response["data"]["encodedContents"] + del response["data"]["contentsEncoded"] + return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} else: return {"success": False, "message": response.get("error", "An unknown error occurred.")} except Exception as e: # Handle Python-side errors (e.g., connection issues) - return {"success": False, "message": f"Python error managing script: {str(e)}"} - - # Potentially add more specific helper tools if needed later, e.g.: - # @mcp.tool() - # def create_script(...): ... - # @mcp.tool() - # def read_script(...): ... - # etc. \ No newline at end of file + return {"success": False, "message": f"Python error managing script: {str(e)}"} \ No newline at end of file diff --git a/Python/tools/read_console.py b/Python/tools/read_console.py index 0de9bac..3d4bd12 100644 --- a/Python/tools/read_console.py +++ b/Python/tools/read_console.py @@ -1,9 +1,9 @@ """ Defines the read_console tool for accessing Unity Editor console messages. """ -from typing import Optional, List, Dict, Any +from typing import List, Dict, Any from mcp.server.fastmcp import FastMCP, Context -from unity_connection import get_unity_connection +from unity_connection import get_unity_connection def register_read_console_tools(mcp: FastMCP): """Registers the read_console tool with the MCP server.""" @@ -11,17 +11,18 @@ def register_read_console_tools(mcp: FastMCP): @mcp.tool() def read_console( ctx: Context, - action: Optional[str] = 'get', - types: Optional[List[str]] = ['error', 'warning', 'log'], - count: Optional[int] = None, - filter_text: Optional[str] = None, - since_timestamp: Optional[str] = None, - format: Optional[str] = 'detailed', - include_stacktrace: Optional[bool] = True, + action: str = None, + types: List[str] = None, + count: int = None, + filter_text: str = None, + since_timestamp: str = None, + format: str = None, + include_stacktrace: bool = None ) -> Dict[str, Any]: """Gets messages from or clears the Unity Editor console. Args: + ctx: The MCP context. action: Operation ('get' or 'clear'). types: Message types to get ('error', 'warning', 'log', 'all'). count: Max messages to return. @@ -37,17 +38,24 @@ def register_read_console_tools(mcp: FastMCP): # Get the connection instance bridge = get_unity_connection() - # Normalize action - action = action.lower() if action else 'get' + # Set defaults if values are None + action = action if action is not None else 'get' + types = types if types is not None else ['error', 'warning', 'log'] + format = format if format is not None else 'detailed' + include_stacktrace = include_stacktrace if include_stacktrace is not None else True + + # Normalize action if it's a string + if isinstance(action, str): + action = action.lower() # Prepare parameters for the C# handler params_dict = { "action": action, - "types": types if types else ['error', 'warning', 'log'], # Ensure types is not None + "types": types, "count": count, "filterText": filter_text, "sinceTimestamp": since_timestamp, - "format": format.lower() if format else 'detailed', + "format": format.lower() if isinstance(format, str) else format, "includeStacktrace": include_stacktrace } @@ -59,6 +67,4 @@ def register_read_console_tools(mcp: FastMCP): params_dict['count'] = None # Forward the command using the bridge's send_command method - # The command type is the name of the tool itself in this case - # No await needed as send_command is synchronous return bridge.send_command("read_console", params_dict) \ No newline at end of file diff --git a/Python/unity_connection.py b/Python/unity_connection.py index 582a6a8..ce30316 100644 --- a/Python/unity_connection.py +++ b/Python/unity_connection.py @@ -125,10 +125,27 @@ class UnityConnection: # Normal command handling command = {"type": command_type, "params": params or {}} try: - logger.info(f"Sending command: {command_type} with params: {params}") - self.sock.sendall(json.dumps(command).encode('utf-8')) + # Check for very large content that might cause JSON issues + command_size = len(json.dumps(command)) + + if command_size > config.buffer_size / 2: + logger.warning(f"Large command detected ({command_size} bytes). This might cause issues.") + + logger.info(f"Sending command: {command_type} with params size: {command_size} bytes") + + # Ensure we have a valid JSON string before sending + command_json = json.dumps(command, ensure_ascii=False) + self.sock.sendall(command_json.encode('utf-8')) + response_data = self.receive_full_response(self.sock) - response = json.loads(response_data.decode('utf-8')) + try: + response = json.loads(response_data.decode('utf-8')) + except json.JSONDecodeError as je: + logger.error(f"JSON decode error: {str(je)}") + # Log partial response for debugging + partial_response = response_data.decode('utf-8')[:500] + "..." if len(response_data) > 500 else response_data.decode('utf-8') + logger.error(f"Partial response: {partial_response}") + raise Exception(f"Invalid JSON response from Unity: {str(je)}") if response.get("status") == "error": error_message = response.get("error") or response.get("message", "Unknown Unity error") diff --git a/climber-prompt.md b/climber-prompt.md new file mode 100644 index 0000000..da07f84 --- /dev/null +++ b/climber-prompt.md @@ -0,0 +1,66 @@ +Follow this detailed step-by-step guide to build this **"Crystal Climber"** game. + +--- + +### Step 1: Set Up the Basic Scene +1. Create a new 3D project named "Crystal Climber." +2. Add a large flat plane as the starting ground (this can act as the base of the climb). +3. Add a simple 3D cube or capsule as the player character. +4. Position the player on the ground plane, slightly above it (to account for gravity). +5. Add a directional light to illuminate the scene evenly. + +--- + +### Step 2: Player Movement Basics +6. Implement basic WASD movement for the player (forward, backward, left, right). +7. Add a jump ability triggered by the spacebar. +8. Attach a third-person camera to follow the player (positioned slightly behind and above). + +--- + +### Step 3: Build the Platform Structure +9. Create a flat, square platform (e.g., a thin cube or plane) as a prefab. +10. Place 5 platforms manually in the scene, staggered vertically and slightly offset horizontally (forming a climbable path upward). +11. Add collision to the platforms so the player can land on them. +12. Test the player jumping from the ground plane to the first platform and up the sequence. + +--- + +### Step 4: Core Objective +13. Place a glowing cube or sphere at the topmost platform as the "crystal." +14. Make the crystal detectable so the game recognizes when the player reaches it. +15. Add a win condition (e.g., display "You Win!" text on screen when the player touches the crystal). + +--- + +### Step 5: Visual Polish +16. Apply a semi-transparent material to the platforms (e.g., light blue with a faint glow). +17. Add a pulsing effect to the platforms (e.g., slight scale increase/decrease or opacity shift). +18. Change the scene background to a starry skybox. +19. Add a particle effect (e.g., sparkles or glowing dots) around the crystal. + +--- + +### Step 6: Refine the Platforms +20. Adjust the spacing between platforms to ensure jumps are challenging but possible. +21. Add 5 more platforms (total 10) to extend the climb vertically. +22. Place a small floating orb or decorative object on one platform as a visual detail. + +--- + +### Step 7: Audio Enhancement +23. Add a looping ambient background sound (e.g., soft wind or ethereal hum). +24. Attach a jump sound to the player (e.g., a light tap or whoosh). +25. Add a short victory sound (e.g., a chime or jingle) when the player reaches the crystal. + +--- + +### Step 8: Final Touches for Devlog Appeal +26. Add a subtle camera zoom-in effect when the player touches the crystal. +27. Sprinkle a few particle effects (e.g., faint stars or mist) across the scene for atmosphere. + +--- + +### Extras +29. Add a double-jump ability (e.g., press space twice) to make platforming easier. +30. Place a slow-rotating spike ball on one platform as a hazard to jump over. \ No newline at end of file diff --git a/climber-prompt.md.meta b/climber-prompt.md.meta new file mode 100644 index 0000000..2a77162 --- /dev/null +++ b/climber-prompt.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 59f0a16c19ac31d48a5b294600c96873 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: