restructured project
- moved Unity package and Python application into separate folders to be downloaded separately using clone .git/UnityMcpBridgemain
parent
9c8ac7a2aa
commit
cb603b7b1a
|
|
@ -14,6 +14,8 @@ build/
|
||||||
dist/
|
dist/
|
||||||
wheels/
|
wheels/
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
UnityMcpServer/**/*.meta
|
||||||
|
UnityMcpServer.meta
|
||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
|
@ -21,6 +23,8 @@ wheels/
|
||||||
# Unity Editor
|
# Unity Editor
|
||||||
*.unitypackage
|
*.unitypackage
|
||||||
*.asset
|
*.asset
|
||||||
|
UnityMcpBridge.meta
|
||||||
|
package.json.meta
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Models
|
|
||||||
{
|
|
||||||
[Serializable]
|
|
||||||
public class MCPConfigServers
|
|
||||||
{
|
|
||||||
[JsonProperty("unityMCP")]
|
|
||||||
public MCPConfigServer unityMCP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
namespace UnityMCP.Editor.Models
|
|
||||||
{
|
|
||||||
// Enum representing the various status states for MCP clients
|
|
||||||
public enum McpStatus
|
|
||||||
{
|
|
||||||
NotConfigured, // Not set up yet
|
|
||||||
Configured, // Successfully configured
|
|
||||||
Running, // Service is running
|
|
||||||
Connected, // Successfully connected
|
|
||||||
IncorrectPath, // Configuration has incorrect paths
|
|
||||||
CommunicationError, // Connected but communication issues
|
|
||||||
NoResponse, // Connected but not responding
|
|
||||||
MissingConfig, // Config file exists but missing required elements
|
|
||||||
UnsupportedOS, // OS is not supported
|
|
||||||
Error // General error state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,959 +0,0 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using UnityMCP.Editor.Helpers; // For Response class
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Tools
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Handles asset management operations within the Unity project.
|
|
||||||
/// </summary>
|
|
||||||
public static class ManageAsset
|
|
||||||
{
|
|
||||||
// --- Main Handler ---
|
|
||||||
|
|
||||||
// Define the list of valid actions
|
|
||||||
private static readonly List<string> ValidActions = new List<string>
|
|
||||||
{
|
|
||||||
"import", "create", "modify", "delete", "duplicate",
|
|
||||||
"move", "rename", "search", "get_info", "create_folder",
|
|
||||||
"get_components"
|
|
||||||
};
|
|
||||||
|
|
||||||
public static object HandleCommand(JObject @params)
|
|
||||||
{
|
|
||||||
string action = @params["action"]?.ToString().ToLower();
|
|
||||||
if (string.IsNullOrEmpty(action))
|
|
||||||
{
|
|
||||||
return Response.Error("Action parameter is required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the action is valid before switching
|
|
||||||
if (!ValidActions.Contains(action))
|
|
||||||
{
|
|
||||||
string validActionsList = string.Join(", ", ValidActions);
|
|
||||||
return Response.Error($"Unknown action: '{action}'. Valid actions are: {validActionsList}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common parameters
|
|
||||||
string path = @params["path"]?.ToString();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
switch (action)
|
|
||||||
{
|
|
||||||
case "import":
|
|
||||||
// Note: Unity typically auto-imports. This might re-import or configure import settings.
|
|
||||||
return ReimportAsset(path, @params["properties"] as JObject);
|
|
||||||
case "create":
|
|
||||||
return CreateAsset(@params);
|
|
||||||
case "modify":
|
|
||||||
return ModifyAsset(path, @params["properties"] as JObject);
|
|
||||||
case "delete":
|
|
||||||
return DeleteAsset(path);
|
|
||||||
case "duplicate":
|
|
||||||
return DuplicateAsset(path, @params["destination"]?.ToString());
|
|
||||||
case "move": // Often same as rename if within Assets/
|
|
||||||
case "rename":
|
|
||||||
return MoveOrRenameAsset(path, @params["destination"]?.ToString());
|
|
||||||
case "search":
|
|
||||||
return SearchAssets(@params);
|
|
||||||
case "get_info":
|
|
||||||
return GetAssetInfo(path, @params["generatePreview"]?.ToObject<bool>() ?? false);
|
|
||||||
case "create_folder": // Added specific action for clarity
|
|
||||||
return CreateFolder(path);
|
|
||||||
case "get_components":
|
|
||||||
return GetComponentsFromAsset(path);
|
|
||||||
|
|
||||||
default:
|
|
||||||
// This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications.
|
|
||||||
string validActionsListDefault = string.Join(", ", ValidActions);
|
|
||||||
return Response.Error($"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.LogError($"[ManageAsset] Action '{action}' failed for path '{path}': {e}");
|
|
||||||
return Response.Error($"Internal error processing action '{action}' on '{path}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Action Implementations ---
|
|
||||||
|
|
||||||
private static object ReimportAsset(string path, JObject properties)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for reimport.");
|
|
||||||
string fullPath = SanitizeAssetPath(path);
|
|
||||||
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// TODO: Apply importer properties before reimporting?
|
|
||||||
// This is complex as it requires getting the AssetImporter, casting it,
|
|
||||||
// applying properties via reflection or specific methods, saving, then reimporting.
|
|
||||||
if (properties != null && properties.HasValues)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet.");
|
|
||||||
// AssetImporter importer = AssetImporter.GetAtPath(fullPath);
|
|
||||||
// if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); }
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
|
|
||||||
// AssetDatabase.Refresh(); // Usually ImportAsset handles refresh
|
|
||||||
return Response.Success($"Asset '{fullPath}' reimported.", GetAssetData(fullPath));
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Response.Error($"Failed to reimport asset '{fullPath}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object CreateAsset(JObject @params)
|
|
||||||
{
|
|
||||||
string path = @params["path"]?.ToString();
|
|
||||||
string assetType = @params["assetType"]?.ToString();
|
|
||||||
JObject properties = @params["properties"] as JObject;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for create.");
|
|
||||||
if (string.IsNullOrEmpty(assetType)) return Response.Error("'assetType' is required for create.");
|
|
||||||
|
|
||||||
string fullPath = SanitizeAssetPath(path);
|
|
||||||
string directory = Path.GetDirectoryName(fullPath);
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory)))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory));
|
|
||||||
AssetDatabase.Refresh(); // Make sure Unity knows about the new folder
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AssetExists(fullPath)) return Response.Error($"Asset already exists at path: {fullPath}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UnityEngine.Object newAsset = null;
|
|
||||||
string lowerAssetType = assetType.ToLowerInvariant();
|
|
||||||
|
|
||||||
// Handle common asset types
|
|
||||||
if (lowerAssetType == "folder")
|
|
||||||
{
|
|
||||||
return CreateFolder(path); // Use dedicated method
|
|
||||||
}
|
|
||||||
else if (lowerAssetType == "material")
|
|
||||||
{
|
|
||||||
Material mat = new Material(Shader.Find("Standard")); // Default shader
|
|
||||||
// TODO: Apply properties from JObject (e.g., shader name, color, texture assignments)
|
|
||||||
if(properties != null) ApplyMaterialProperties(mat, properties);
|
|
||||||
AssetDatabase.CreateAsset(mat, fullPath);
|
|
||||||
newAsset = mat;
|
|
||||||
}
|
|
||||||
else if (lowerAssetType == "scriptableobject")
|
|
||||||
{
|
|
||||||
string scriptClassName = properties?["scriptClass"]?.ToString();
|
|
||||||
if (string.IsNullOrEmpty(scriptClassName)) return Response.Error("'scriptClass' property required when creating ScriptableObject asset.");
|
|
||||||
|
|
||||||
Type scriptType = FindType(scriptClassName);
|
|
||||||
if (scriptType == null || !typeof(ScriptableObject).IsAssignableFrom(scriptType))
|
|
||||||
{
|
|
||||||
return Response.Error($"Script class '{scriptClassName}' not found or does not inherit from ScriptableObject.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ScriptableObject so = ScriptableObject.CreateInstance(scriptType);
|
|
||||||
// TODO: Apply properties from JObject to the ScriptableObject instance?
|
|
||||||
AssetDatabase.CreateAsset(so, fullPath);
|
|
||||||
newAsset = so;
|
|
||||||
}
|
|
||||||
else if (lowerAssetType == "prefab")
|
|
||||||
{
|
|
||||||
// Creating prefabs usually involves saving an existing GameObject hierarchy.
|
|
||||||
// A common pattern is to create an empty GameObject, configure it, and then save it.
|
|
||||||
return Response.Error("Creating prefabs programmatically usually requires a source GameObject. Use manage_gameobject to create/configure, then save as prefab via a separate mechanism or future enhancement.");
|
|
||||||
// Example (conceptual):
|
|
||||||
// GameObject source = GameObject.Find(properties["sourceGameObject"].ToString());
|
|
||||||
// if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath);
|
|
||||||
}
|
|
||||||
// TODO: Add more asset types (Animation Controller, Scene, etc.)
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Generic creation attempt (might fail or create empty files)
|
|
||||||
// For some types, just creating the file might be enough if Unity imports it.
|
|
||||||
// File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close();
|
|
||||||
// AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it
|
|
||||||
// newAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
|
|
||||||
return Response.Error($"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, ScriptableObject.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newAsset == null && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath))) // Check if it wasn't a folder and asset wasn't created
|
|
||||||
{
|
|
||||||
return Response.Error($"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details.");
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetDatabase.SaveAssets();
|
|
||||||
// AssetDatabase.Refresh(); // CreateAsset often handles refresh
|
|
||||||
return Response.Success($"Asset '{fullPath}' created successfully.", GetAssetData(fullPath));
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Response.Error($"Failed to create asset at '{fullPath}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object CreateFolder(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for create_folder.");
|
|
||||||
string fullPath = SanitizeAssetPath(path);
|
|
||||||
string parentDir = Path.GetDirectoryName(fullPath);
|
|
||||||
string folderName = Path.GetFileName(fullPath);
|
|
||||||
|
|
||||||
if (AssetExists(fullPath))
|
|
||||||
{
|
|
||||||
// Check if it's actually a folder already
|
|
||||||
if (AssetDatabase.IsValidFolder(fullPath))
|
|
||||||
{
|
|
||||||
return Response.Success($"Folder already exists at path: {fullPath}", GetAssetData(fullPath));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Response.Error($"An asset (not a folder) already exists at path: {fullPath}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Ensure parent exists
|
|
||||||
if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir)) {
|
|
||||||
// Recursively create parent folders if needed (AssetDatabase handles this internally)
|
|
||||||
// Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
string guid = AssetDatabase.CreateFolder(parentDir, folderName);
|
|
||||||
if (string.IsNullOrEmpty(guid)) {
|
|
||||||
return Response.Error($"Failed to create folder '{fullPath}'. Check logs and permissions.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssetDatabase.Refresh(); // CreateFolder usually handles refresh
|
|
||||||
return Response.Success($"Folder '{fullPath}' created successfully.", GetAssetData(fullPath));
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Response.Error($"Failed to create folder '{fullPath}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object ModifyAsset(string path, JObject properties)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for modify.");
|
|
||||||
if (properties == null || !properties.HasValues) return Response.Error("'properties' are required for modify.");
|
|
||||||
|
|
||||||
string fullPath = SanitizeAssetPath(path);
|
|
||||||
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
|
|
||||||
if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}");
|
|
||||||
|
|
||||||
bool modified = false; // Flag to track if any changes were made
|
|
||||||
|
|
||||||
// --- NEW: Handle GameObject / Prefab Component Modification ---
|
|
||||||
if (asset is GameObject gameObject)
|
|
||||||
{
|
|
||||||
// Iterate through the properties JSON: keys are component names, values are properties objects for that component
|
|
||||||
foreach (var prop in properties.Properties())
|
|
||||||
{
|
|
||||||
string componentName = prop.Name; // e.g., "Collectible"
|
|
||||||
// Check if the value associated with the component name is actually an object containing properties
|
|
||||||
if (prop.Value is JObject componentProperties && componentProperties.HasValues) // e.g., {"bobSpeed": 2.0}
|
|
||||||
{
|
|
||||||
// Find the component on the GameObject using the name from the JSON key
|
|
||||||
// Using GetComponent(string) is convenient but might require exact type name or be ambiguous.
|
|
||||||
// Consider using FindType helper if needed for more complex scenarios.
|
|
||||||
Component targetComponent = gameObject.GetComponent(componentName);
|
|
||||||
|
|
||||||
if (targetComponent != null)
|
|
||||||
{
|
|
||||||
// Apply the nested properties (e.g., bobSpeed) to the found component instance
|
|
||||||
// Use |= to ensure 'modified' becomes true if any component is successfully modified
|
|
||||||
modified |= ApplyObjectProperties(targetComponent, componentProperties);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Log a warning if a specified component couldn't be found
|
|
||||||
Debug.LogWarning($"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Log a warning if the structure isn't {"ComponentName": {"prop": value}}
|
|
||||||
// We could potentially try to apply this property directly to the GameObject here if needed,
|
|
||||||
// but the primary goal is component modification.
|
|
||||||
Debug.LogWarning($"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Note: 'modified' is now true if ANY component property was successfully changed.
|
|
||||||
}
|
|
||||||
// --- End NEW ---
|
|
||||||
|
|
||||||
// --- Existing logic for other asset types (now as else-if) ---
|
|
||||||
// Example: Modifying a Material
|
|
||||||
else if (asset is Material material)
|
|
||||||
{
|
|
||||||
// Apply properties directly to the material. If this modifies, it sets modified=true.
|
|
||||||
// Use |= in case the asset was already marked modified by previous logic (though unlikely here)
|
|
||||||
modified |= ApplyMaterialProperties(material, properties);
|
|
||||||
}
|
|
||||||
// Example: Modifying a ScriptableObject
|
|
||||||
else if (asset is ScriptableObject so)
|
|
||||||
{
|
|
||||||
// Apply properties directly to the ScriptableObject.
|
|
||||||
modified |= ApplyObjectProperties(so, properties); // General helper
|
|
||||||
}
|
|
||||||
// Example: Modifying TextureImporter settings
|
|
||||||
else if (asset is Texture) {
|
|
||||||
AssetImporter importer = AssetImporter.GetAtPath(fullPath);
|
|
||||||
if (importer is TextureImporter textureImporter)
|
|
||||||
{
|
|
||||||
bool importerModified = ApplyObjectProperties(textureImporter, properties);
|
|
||||||
if (importerModified) {
|
|
||||||
// Importer settings need saving and reimporting
|
|
||||||
AssetDatabase.WriteImportSettingsIfDirty(fullPath);
|
|
||||||
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes
|
|
||||||
modified = true; // Mark overall operation as modified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Debug.LogWarning($"Could not get TextureImporter for {fullPath}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.)
|
|
||||||
else // Fallback for other asset types OR direct properties on non-GameObject assets
|
|
||||||
{
|
|
||||||
// This block handles non-GameObject/Material/ScriptableObject/Texture assets.
|
|
||||||
// Attempts to apply properties directly to the asset itself.
|
|
||||||
Debug.LogWarning($"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself.");
|
|
||||||
modified |= ApplyObjectProperties(asset, properties);
|
|
||||||
}
|
|
||||||
// --- End Existing Logic ---
|
|
||||||
|
|
||||||
// Check if any modification happened (either component or direct asset modification)
|
|
||||||
if (modified)
|
|
||||||
{
|
|
||||||
// Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it.
|
|
||||||
EditorUtility.SetDirty(asset);
|
|
||||||
// Save all modified assets to disk.
|
|
||||||
AssetDatabase.SaveAssets();
|
|
||||||
// Refresh might be needed in some edge cases, but SaveAssets usually covers it.
|
|
||||||
// AssetDatabase.Refresh();
|
|
||||||
return Response.Success($"Asset '{fullPath}' modified successfully.", GetAssetData(fullPath));
|
|
||||||
} else {
|
|
||||||
// If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed.
|
|
||||||
return Response.Success($"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.", GetAssetData(fullPath));
|
|
||||||
// Previous message: return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
// Log the detailed error internally
|
|
||||||
Debug.LogError($"[ManageAsset] Action 'modify' failed for path '{path}': {e}");
|
|
||||||
// Return a user-friendly error message
|
|
||||||
return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object DeleteAsset(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for delete.");
|
|
||||||
string fullPath = SanitizeAssetPath(path);
|
|
||||||
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bool success = AssetDatabase.DeleteAsset(fullPath);
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
// AssetDatabase.Refresh(); // DeleteAsset usually handles refresh
|
|
||||||
return Response.Success($"Asset '{fullPath}' deleted successfully.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This might happen if the file couldn't be deleted (e.g., locked)
|
|
||||||
return Response.Error($"Failed to delete asset '{fullPath}'. Check logs or if the file is locked.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Response.Error($"Error deleting asset '{fullPath}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object DuplicateAsset(string path, string destinationPath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for duplicate.");
|
|
||||||
|
|
||||||
string sourcePath = SanitizeAssetPath(path);
|
|
||||||
if (!AssetExists(sourcePath)) return Response.Error($"Source asset not found at path: {sourcePath}");
|
|
||||||
|
|
||||||
string destPath;
|
|
||||||
if (string.IsNullOrEmpty(destinationPath))
|
|
||||||
{
|
|
||||||
// Generate a unique path if destination is not provided
|
|
||||||
destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
destPath = SanitizeAssetPath(destinationPath);
|
|
||||||
if (AssetExists(destPath)) return Response.Error($"Asset already exists at destination path: {destPath}");
|
|
||||||
// Ensure destination directory exists
|
|
||||||
EnsureDirectoryExists(Path.GetDirectoryName(destPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bool success = AssetDatabase.CopyAsset(sourcePath, destPath);
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
// AssetDatabase.Refresh();
|
|
||||||
return Response.Success($"Asset '{sourcePath}' duplicated to '{destPath}'.", GetAssetData(destPath));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Response.Error($"Failed to duplicate asset from '{sourcePath}' to '{destPath}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Response.Error($"Error duplicating asset '{sourcePath}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object MoveOrRenameAsset(string path, string destinationPath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for move/rename.");
|
|
||||||
if (string.IsNullOrEmpty(destinationPath)) return Response.Error("'destination' path is required for move/rename.");
|
|
||||||
|
|
||||||
string sourcePath = SanitizeAssetPath(path);
|
|
||||||
string destPath = SanitizeAssetPath(destinationPath);
|
|
||||||
|
|
||||||
if (!AssetExists(sourcePath)) return Response.Error($"Source asset not found at path: {sourcePath}");
|
|
||||||
if (AssetExists(destPath)) return Response.Error($"An asset already exists at the destination path: {destPath}");
|
|
||||||
|
|
||||||
// Ensure destination directory exists
|
|
||||||
EnsureDirectoryExists(Path.GetDirectoryName(destPath));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Validate will return an error string if failed, null if successful
|
|
||||||
string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath);
|
|
||||||
if (!string.IsNullOrEmpty(error))
|
|
||||||
{
|
|
||||||
return Response.Error($"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}");
|
|
||||||
}
|
|
||||||
|
|
||||||
string guid = AssetDatabase.MoveAsset(sourcePath, destPath);
|
|
||||||
if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success
|
|
||||||
{
|
|
||||||
// AssetDatabase.Refresh(); // MoveAsset usually handles refresh
|
|
||||||
return Response.Success($"Asset moved/renamed from '{sourcePath}' to '{destPath}'.", GetAssetData(destPath));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This case might not be reachable if ValidateMoveAsset passes, but good to have
|
|
||||||
return Response.Error($"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Response.Error($"Error moving/renaming asset '{sourcePath}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object SearchAssets(JObject @params)
|
|
||||||
{
|
|
||||||
string searchPattern = @params["searchPattern"]?.ToString();
|
|
||||||
string filterType = @params["filterType"]?.ToString();
|
|
||||||
string pathScope = @params["path"]?.ToString(); // Use path as folder scope
|
|
||||||
string filterDateAfterStr = @params["filterDateAfter"]?.ToString();
|
|
||||||
int pageSize = @params["pageSize"]?.ToObject<int?>() ?? 50; // Default page size
|
|
||||||
int pageNumber = @params["pageNumber"]?.ToObject<int?>() ?? 1; // Default page number (1-based)
|
|
||||||
bool generatePreview = @params["generatePreview"]?.ToObject<bool>() ?? false;
|
|
||||||
|
|
||||||
List<string> searchFilters = new List<string>();
|
|
||||||
if (!string.IsNullOrEmpty(searchPattern)) searchFilters.Add(searchPattern);
|
|
||||||
if (!string.IsNullOrEmpty(filterType)) searchFilters.Add($"t:{filterType}");
|
|
||||||
|
|
||||||
string[] folderScope = null;
|
|
||||||
if (!string.IsNullOrEmpty(pathScope))
|
|
||||||
{
|
|
||||||
folderScope = new string[] { SanitizeAssetPath(pathScope) };
|
|
||||||
if (!AssetDatabase.IsValidFolder(folderScope[0])) {
|
|
||||||
// Maybe the user provided a file path instead of a folder?
|
|
||||||
// We could search in the containing folder, or return an error.
|
|
||||||
Debug.LogWarning($"Search path '{folderScope[0]}' is not a valid folder. Searching entire project.");
|
|
||||||
folderScope = null; // Search everywhere if path isn't a folder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime? filterDateAfter = null;
|
|
||||||
if (!string.IsNullOrEmpty(filterDateAfterStr)) {
|
|
||||||
if (DateTime.TryParse(filterDateAfterStr, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime parsedDate)) {
|
|
||||||
filterDateAfter = parsedDate;
|
|
||||||
} else {
|
|
||||||
Debug.LogWarning($"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string[] guids = AssetDatabase.FindAssets(string.Join(" ", searchFilters), folderScope);
|
|
||||||
List<object> results = new List<object>();
|
|
||||||
int totalFound = 0;
|
|
||||||
|
|
||||||
foreach (string guid in guids)
|
|
||||||
{
|
|
||||||
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
|
||||||
if (string.IsNullOrEmpty(assetPath)) continue;
|
|
||||||
|
|
||||||
// Apply date filter if present
|
|
||||||
if (filterDateAfter.HasValue) {
|
|
||||||
DateTime lastWriteTime = File.GetLastWriteTimeUtc(Path.Combine(Directory.GetCurrentDirectory(), assetPath));
|
|
||||||
if (lastWriteTime <= filterDateAfter.Value) {
|
|
||||||
continue; // Skip assets older than or equal to the filter date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalFound++; // Count matching assets before pagination
|
|
||||||
results.Add(GetAssetData(assetPath, generatePreview));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply pagination
|
|
||||||
int startIndex = (pageNumber - 1) * pageSize;
|
|
||||||
var pagedResults = results.Skip(startIndex).Take(pageSize).ToList();
|
|
||||||
|
|
||||||
return Response.Success($"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).", new {
|
|
||||||
totalAssets = totalFound,
|
|
||||||
pageSize = pageSize,
|
|
||||||
pageNumber = pageNumber,
|
|
||||||
assets = pagedResults
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Response.Error($"Error searching assets: {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object GetAssetInfo(string path, bool generatePreview)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for get_info.");
|
|
||||||
string fullPath = SanitizeAssetPath(path);
|
|
||||||
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Response.Success("Asset info retrieved.", GetAssetData(fullPath, generatePreview));
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Response.Error($"Error getting info for asset '{fullPath}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves components attached to a GameObject asset (like a Prefab).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The asset path of the GameObject or Prefab.</param>
|
|
||||||
/// <returns>A response object containing a list of component type names or an error.</returns>
|
|
||||||
private static object GetComponentsFromAsset(string path)
|
|
||||||
{
|
|
||||||
// 1. Validate input path
|
|
||||||
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for get_components.");
|
|
||||||
|
|
||||||
// 2. Sanitize and check existence
|
|
||||||
string fullPath = SanitizeAssetPath(path);
|
|
||||||
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 3. Load the asset
|
|
||||||
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
|
|
||||||
if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}");
|
|
||||||
|
|
||||||
// 4. Check if it's a GameObject (Prefabs load as GameObjects)
|
|
||||||
GameObject gameObject = asset as GameObject;
|
|
||||||
if (gameObject == null)
|
|
||||||
{
|
|
||||||
// Also check if it's *directly* a Component type (less common for primary assets)
|
|
||||||
Component componentAsset = asset as Component;
|
|
||||||
if (componentAsset != null) {
|
|
||||||
// If the asset itself *is* a component, maybe return just its info?
|
|
||||||
// This is an edge case. Let's stick to GameObjects for now.
|
|
||||||
return Response.Error($"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject.");
|
|
||||||
}
|
|
||||||
return Response.Error($"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Get components
|
|
||||||
Component[] components = gameObject.GetComponents<Component>();
|
|
||||||
|
|
||||||
// 6. Format component data
|
|
||||||
List<object> componentList = components.Select(comp => new {
|
|
||||||
typeName = comp.GetType().FullName,
|
|
||||||
instanceID = comp.GetInstanceID(),
|
|
||||||
// TODO: Add more component-specific details here if needed in the future?
|
|
||||||
// Requires reflection or specific handling per component type.
|
|
||||||
}).ToList<object>(); // Explicit cast for clarity if needed
|
|
||||||
|
|
||||||
// 7. Return success response
|
|
||||||
return Response.Success($"Found {componentList.Count} component(s) on asset '{fullPath}'.", componentList);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.LogError($"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}");
|
|
||||||
return Response.Error($"Error getting components for asset '{fullPath}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Internal Helpers ---
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures the asset path starts with "Assets/".
|
|
||||||
/// </summary>
|
|
||||||
private static string SanitizeAssetPath(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path)) return path;
|
|
||||||
path = path.Replace('\\', '/'); // Normalize separators
|
|
||||||
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return "Assets/" + path.TrimStart('/');
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if an asset exists at the given path (file or folder).
|
|
||||||
/// </summary>
|
|
||||||
private static bool AssetExists(string sanitizedPath)
|
|
||||||
{
|
|
||||||
// AssetDatabase APIs are generally preferred over raw File/Directory checks for assets.
|
|
||||||
// Check if it's a known asset GUID.
|
|
||||||
if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath)))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// AssetPathToGUID might not work for newly created folders not yet refreshed.
|
|
||||||
// Check directory explicitly for folders.
|
|
||||||
if(Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) {
|
|
||||||
// Check if it's considered a *valid* folder by Unity
|
|
||||||
return AssetDatabase.IsValidFolder(sanitizedPath);
|
|
||||||
}
|
|
||||||
// Check file existence for non-folder assets.
|
|
||||||
if(File.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))){
|
|
||||||
return true; // Assume if file exists, it's an asset or will be imported
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
// Alternative: return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures the directory for a given asset path exists, creating it if necessary.
|
|
||||||
/// </summary>
|
|
||||||
private static void EnsureDirectoryExists(string directoryPath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(directoryPath)) return;
|
|
||||||
string fullDirPath = Path.Combine(Directory.GetCurrentDirectory(), directoryPath);
|
|
||||||
if (!Directory.Exists(fullDirPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(fullDirPath);
|
|
||||||
AssetDatabase.Refresh(); // Let Unity know about the new folder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Applies properties from JObject to a Material.
|
|
||||||
/// </summary>
|
|
||||||
private static bool ApplyMaterialProperties(Material mat, JObject properties)
|
|
||||||
{
|
|
||||||
if (mat == null || properties == null) return false;
|
|
||||||
bool modified = false;
|
|
||||||
|
|
||||||
// Example: Set shader
|
|
||||||
if (properties["shader"]?.Type == JTokenType.String) {
|
|
||||||
Shader newShader = Shader.Find(properties["shader"].ToString());
|
|
||||||
if (newShader != null && mat.shader != newShader) {
|
|
||||||
mat.shader = newShader;
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Example: Set color property
|
|
||||||
if (properties["color"] is JObject colorProps) {
|
|
||||||
string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color
|
|
||||||
if (colorProps["value"] is JArray colArr && colArr.Count >= 3) {
|
|
||||||
try {
|
|
||||||
Color newColor = new Color(
|
|
||||||
colArr[0].ToObject<float>(),
|
|
||||||
colArr[1].ToObject<float>(),
|
|
||||||
colArr[2].ToObject<float>(),
|
|
||||||
colArr.Count > 3 ? colArr[3].ToObject<float>() : 1.0f
|
|
||||||
);
|
|
||||||
if(mat.HasProperty(propName) && mat.GetColor(propName) != newColor) {
|
|
||||||
mat.SetColor(propName, newColor);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
} catch (Exception ex) { Debug.LogWarning($"Error parsing color property '{propName}': {ex.Message}"); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Example: Set float property
|
|
||||||
if (properties["float"] is JObject floatProps) {
|
|
||||||
string propName = floatProps["name"]?.ToString();
|
|
||||||
if (!string.IsNullOrEmpty(propName) && floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer) {
|
|
||||||
try {
|
|
||||||
float newVal = floatProps["value"].ToObject<float>();
|
|
||||||
if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal) {
|
|
||||||
mat.SetFloat(propName, newVal);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
} catch (Exception ex) { Debug.LogWarning($"Error parsing float property '{propName}': {ex.Message}"); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Example: Set texture property
|
|
||||||
if (properties["texture"] is JObject texProps) {
|
|
||||||
string propName = texProps["name"]?.ToString() ?? "_MainTex"; // Default main texture
|
|
||||||
string texPath = texProps["path"]?.ToString();
|
|
||||||
if (!string.IsNullOrEmpty(texPath)) {
|
|
||||||
Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(SanitizeAssetPath(texPath));
|
|
||||||
if (newTex != null && mat.HasProperty(propName) && mat.GetTexture(propName) != newTex) {
|
|
||||||
mat.SetTexture(propName, newTex);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
else if(newTex == null) {
|
|
||||||
Debug.LogWarning($"Texture not found at path: {texPath}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
|
|
||||||
return modified;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generic helper to set properties on any UnityEngine.Object using reflection.
|
|
||||||
/// </summary>
|
|
||||||
private static bool ApplyObjectProperties(UnityEngine.Object target, JObject properties)
|
|
||||||
{
|
|
||||||
if (target == null || properties == null) return false;
|
|
||||||
bool modified = false;
|
|
||||||
Type type = target.GetType();
|
|
||||||
|
|
||||||
foreach (var prop in properties.Properties())
|
|
||||||
{
|
|
||||||
string propName = prop.Name;
|
|
||||||
JToken propValue = prop.Value;
|
|
||||||
if (SetPropertyOrField(target, propName, propValue, type))
|
|
||||||
{
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return modified;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper to set a property or field via reflection, handling basic types and Unity objects.
|
|
||||||
/// </summary>
|
|
||||||
private static bool SetPropertyOrField(object target, string memberName, JToken value, Type type = null)
|
|
||||||
{
|
|
||||||
type = type ?? target.GetType();
|
|
||||||
System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags);
|
|
||||||
if (propInfo != null && propInfo.CanWrite)
|
|
||||||
{
|
|
||||||
object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType);
|
|
||||||
if (convertedValue != null && !object.Equals(propInfo.GetValue(target), convertedValue))
|
|
||||||
{
|
|
||||||
propInfo.SetValue(target, convertedValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags);
|
|
||||||
if (fieldInfo != null)
|
|
||||||
{
|
|
||||||
object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType);
|
|
||||||
if (convertedValue != null && !object.Equals(fieldInfo.GetValue(target), convertedValue))
|
|
||||||
{
|
|
||||||
fieldInfo.SetValue(target, convertedValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"[SetPropertyOrField] Failed to set '{memberName}' on {type.Name}: {ex.Message}");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Simple JToken to Type conversion for common Unity types and primitives.
|
|
||||||
/// </summary>
|
|
||||||
private static object ConvertJTokenToType(JToken token, Type targetType)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (token == null || token.Type == JTokenType.Null) return null;
|
|
||||||
|
|
||||||
if (targetType == typeof(string)) return token.ToObject<string>();
|
|
||||||
if (targetType == typeof(int)) return token.ToObject<int>();
|
|
||||||
if (targetType == typeof(float)) return token.ToObject<float>();
|
|
||||||
if (targetType == typeof(bool)) return token.ToObject<bool>();
|
|
||||||
if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2)
|
|
||||||
return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>());
|
|
||||||
if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3)
|
|
||||||
return new Vector3(arrV3[0].ToObject<float>(), arrV3[1].ToObject<float>(), arrV3[2].ToObject<float>());
|
|
||||||
if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4)
|
|
||||||
return new Vector4(arrV4[0].ToObject<float>(), arrV4[1].ToObject<float>(), arrV4[2].ToObject<float>(), arrV4[3].ToObject<float>());
|
|
||||||
if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4)
|
|
||||||
return new Quaternion(arrQ[0].ToObject<float>(), arrQ[1].ToObject<float>(), arrQ[2].ToObject<float>(), arrQ[3].ToObject<float>());
|
|
||||||
if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA
|
|
||||||
return new Color(arrC[0].ToObject<float>(), arrC[1].ToObject<float>(), arrC[2].ToObject<float>(), arrC.Count > 3 ? arrC[3].ToObject<float>() : 1.0f);
|
|
||||||
if (targetType.IsEnum)
|
|
||||||
return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing
|
|
||||||
|
|
||||||
// Handle loading Unity Objects (Materials, Textures, etc.) by path
|
|
||||||
if (typeof(UnityEngine.Object).IsAssignableFrom(targetType) && token.Type == JTokenType.String)
|
|
||||||
{
|
|
||||||
string assetPath = SanitizeAssetPath(token.ToString());
|
|
||||||
UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(assetPath, targetType);
|
|
||||||
if (loadedAsset == null) {
|
|
||||||
Debug.LogWarning($"[ConvertJTokenToType] Could not load asset of type {targetType.Name} from path: {assetPath}");
|
|
||||||
}
|
|
||||||
return loadedAsset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Try direct conversion (might work for other simple value types)
|
|
||||||
return token.ToObject(targetType);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"[ConvertJTokenToType] Could not convert JToken '{token}' (type {token.Type}) to type '{targetType.Name}': {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper to find a Type by name, searching relevant assemblies.
|
|
||||||
/// Needed for creating ScriptableObjects or finding component types by name.
|
|
||||||
/// </summary>
|
|
||||||
private static Type FindType(string typeName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(typeName)) return null;
|
|
||||||
|
|
||||||
// Try direct lookup first (common Unity types often don't need assembly qualified name)
|
|
||||||
var type = Type.GetType(typeName) ??
|
|
||||||
Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule") ??
|
|
||||||
Type.GetType($"UnityEngine.UI.{typeName}, UnityEngine.UI") ??
|
|
||||||
Type.GetType($"UnityEditor.{typeName}, UnityEditor.CoreModule");
|
|
||||||
|
|
||||||
if (type != null) return type;
|
|
||||||
|
|
||||||
// If not found, search loaded assemblies (slower but more robust for user scripts)
|
|
||||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
|
||||||
{
|
|
||||||
// Look for non-namespaced first
|
|
||||||
type = assembly.GetType(typeName, false, true); // throwOnError=false, ignoreCase=true
|
|
||||||
if (type != null) return type;
|
|
||||||
|
|
||||||
// Check common namespaces if simple name given
|
|
||||||
type = assembly.GetType("UnityEngine." + typeName, false, true);
|
|
||||||
if (type != null) return type;
|
|
||||||
type = assembly.GetType("UnityEditor." + typeName, false, true);
|
|
||||||
if (type != null) return type;
|
|
||||||
// Add other likely namespaces if needed (e.g., specific plugins)
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.LogWarning($"[FindType] Type '{typeName}' not found in any loaded assembly.");
|
|
||||||
return null; // Not found
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Data Serialization ---
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a serializable representation of an asset.
|
|
||||||
/// </summary>
|
|
||||||
private static object GetAssetData(string path, bool generatePreview = false)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path) || !AssetExists(path)) return null;
|
|
||||||
|
|
||||||
string guid = AssetDatabase.AssetPathToGUID(path);
|
|
||||||
Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path);
|
|
||||||
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
|
|
||||||
string previewBase64 = null;
|
|
||||||
int previewWidth = 0;
|
|
||||||
int previewHeight = 0;
|
|
||||||
|
|
||||||
if (generatePreview && asset != null)
|
|
||||||
{
|
|
||||||
Texture2D preview = AssetPreview.GetAssetPreview(asset);
|
|
||||||
|
|
||||||
if (preview != null)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
// Ensure texture is readable for EncodeToPNG
|
|
||||||
// Creating a temporary readable copy is safer
|
|
||||||
RenderTexture rt = RenderTexture.GetTemporary(preview.width, preview.height);
|
|
||||||
Graphics.Blit(preview, rt);
|
|
||||||
RenderTexture previous = RenderTexture.active;
|
|
||||||
RenderTexture.active = rt;
|
|
||||||
Texture2D readablePreview = new Texture2D(preview.width, preview.height);
|
|
||||||
readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
|
|
||||||
readablePreview.Apply();
|
|
||||||
RenderTexture.active = previous;
|
|
||||||
RenderTexture.ReleaseTemporary(rt);
|
|
||||||
|
|
||||||
byte[] pngData = readablePreview.EncodeToPNG();
|
|
||||||
previewBase64 = Convert.ToBase64String(pngData);
|
|
||||||
previewWidth = readablePreview.width;
|
|
||||||
previewHeight = readablePreview.height;
|
|
||||||
UnityEngine.Object.DestroyImmediate(readablePreview); // Clean up temp texture
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Debug.LogWarning($"Failed to generate readable preview for '{path}': {ex.Message}. Preview might not be readable.");
|
|
||||||
// Fallback: Try getting static preview if available?
|
|
||||||
// Texture2D staticPreview = AssetPreview.GetMiniThumbnail(asset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"Could not get asset preview for {path} (Type: {assetType?.Name}). Is it supported?");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
path = path,
|
|
||||||
guid = guid,
|
|
||||||
assetType = assetType?.FullName ?? "Unknown",
|
|
||||||
name = Path.GetFileNameWithoutExtension(path),
|
|
||||||
fileName = Path.GetFileName(path),
|
|
||||||
isFolder = AssetDatabase.IsValidFolder(path),
|
|
||||||
instanceID = asset?.GetInstanceID() ?? 0,
|
|
||||||
lastWriteTimeUtc = File.GetLastWriteTimeUtc(Path.Combine(Directory.GetCurrentDirectory(), path)).ToString("o"), // ISO 8601
|
|
||||||
// --- Preview Data ---
|
|
||||||
previewBase64 = previewBase64, // PNG data as Base64 string
|
|
||||||
previewWidth = previewWidth,
|
|
||||||
previewHeight = previewHeight
|
|
||||||
// TODO: Add more metadata? Importer settings? Dependencies?
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,397 +0,0 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditorInternal;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using UnityMCP.Editor.Helpers; // For Response class
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Tools
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Handles reading and clearing Unity Editor console log entries.
|
|
||||||
/// Uses reflection to access internal LogEntry methods/properties.
|
|
||||||
/// </summary>
|
|
||||||
public static class ReadConsole
|
|
||||||
{
|
|
||||||
// Reflection members for accessing internal LogEntry data
|
|
||||||
// private static MethodInfo _getEntriesMethod; // Removed as it's unused and fails reflection
|
|
||||||
private static MethodInfo _startGettingEntriesMethod;
|
|
||||||
private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End...
|
|
||||||
private static MethodInfo _clearMethod;
|
|
||||||
private static MethodInfo _getCountMethod;
|
|
||||||
private static MethodInfo _getEntryMethod;
|
|
||||||
private static FieldInfo _modeField;
|
|
||||||
private static FieldInfo _messageField;
|
|
||||||
private static FieldInfo _fileField;
|
|
||||||
private static FieldInfo _lineField;
|
|
||||||
private static FieldInfo _instanceIdField;
|
|
||||||
// Note: Timestamp is not directly available in LogEntry; need to parse message or find alternative?
|
|
||||||
|
|
||||||
// Static constructor for reflection setup
|
|
||||||
static ReadConsole()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
Type logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
|
|
||||||
if (logEntriesType == null) throw new Exception("Could not find internal type UnityEditor.LogEntries");
|
|
||||||
|
|
||||||
// Include NonPublic binding flags as internal APIs might change accessibility
|
|
||||||
BindingFlags staticFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
|
|
||||||
BindingFlags instanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
|
||||||
|
|
||||||
_startGettingEntriesMethod = logEntriesType.GetMethod("StartGettingEntries", staticFlags);
|
|
||||||
if (_startGettingEntriesMethod == null) throw new Exception("Failed to reflect LogEntries.StartGettingEntries");
|
|
||||||
|
|
||||||
// Try reflecting EndGettingEntries based on warning message
|
|
||||||
_endGettingEntriesMethod = logEntriesType.GetMethod("EndGettingEntries", staticFlags);
|
|
||||||
if (_endGettingEntriesMethod == null) throw new Exception("Failed to reflect LogEntries.EndGettingEntries");
|
|
||||||
|
|
||||||
_clearMethod = logEntriesType.GetMethod("Clear", staticFlags);
|
|
||||||
if (_clearMethod == null) throw new Exception("Failed to reflect LogEntries.Clear");
|
|
||||||
|
|
||||||
_getCountMethod = logEntriesType.GetMethod("GetCount", staticFlags);
|
|
||||||
if (_getCountMethod == null) throw new Exception("Failed to reflect LogEntries.GetCount");
|
|
||||||
|
|
||||||
_getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", staticFlags);
|
|
||||||
if (_getEntryMethod == null) throw new Exception("Failed to reflect LogEntries.GetEntryInternal");
|
|
||||||
|
|
||||||
Type logEntryType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntry");
|
|
||||||
if (logEntryType == null) throw new Exception("Could not find internal type UnityEditor.LogEntry");
|
|
||||||
|
|
||||||
_modeField = logEntryType.GetField("mode", instanceFlags);
|
|
||||||
if (_modeField == null) throw new Exception("Failed to reflect LogEntry.mode");
|
|
||||||
|
|
||||||
_messageField = logEntryType.GetField("message", instanceFlags);
|
|
||||||
if (_messageField == null) throw new Exception("Failed to reflect LogEntry.message");
|
|
||||||
|
|
||||||
_fileField = logEntryType.GetField("file", instanceFlags);
|
|
||||||
if (_fileField == null) throw new Exception("Failed to reflect LogEntry.file");
|
|
||||||
|
|
||||||
_lineField = logEntryType.GetField("line", instanceFlags);
|
|
||||||
if (_lineField == null) throw new Exception("Failed to reflect LogEntry.line");
|
|
||||||
|
|
||||||
_instanceIdField = logEntryType.GetField("instanceID", instanceFlags);
|
|
||||||
if (_instanceIdField == null) throw new Exception("Failed to reflect LogEntry.instanceID");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.LogError($"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries/LogEntry. Console reading/clearing will likely fail. Specific Error: {e.Message}");
|
|
||||||
// Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this.
|
|
||||||
_startGettingEntriesMethod = _endGettingEntriesMethod = _clearMethod = _getCountMethod = _getEntryMethod = null;
|
|
||||||
_modeField = _messageField = _fileField = _lineField = _instanceIdField = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Main Handler ---
|
|
||||||
|
|
||||||
public static object HandleCommand(JObject @params)
|
|
||||||
{
|
|
||||||
// Check if ALL required reflection members were successfully initialized.
|
|
||||||
if (_startGettingEntriesMethod == null || _endGettingEntriesMethod == null ||
|
|
||||||
_clearMethod == null || _getCountMethod == null || _getEntryMethod == null ||
|
|
||||||
_modeField == null || _messageField == null || _fileField == null || _lineField == null || _instanceIdField == null)
|
|
||||||
{
|
|
||||||
// Log the error here as well for easier debugging in Unity Console
|
|
||||||
Debug.LogError("[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue.");
|
|
||||||
return Response.Error("ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string action = @params["action"]?.ToString().ToLower() ?? "get";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (action == "clear")
|
|
||||||
{
|
|
||||||
return ClearConsole();
|
|
||||||
}
|
|
||||||
else if (action == "get")
|
|
||||||
{
|
|
||||||
// Extract parameters for 'get'
|
|
||||||
var types = (@params["types"] as JArray)?.Select(t => t.ToString().ToLower()).ToList() ?? new List<string> { "error", "warning", "log" };
|
|
||||||
int? count = @params["count"]?.ToObject<int?>();
|
|
||||||
string filterText = @params["filterText"]?.ToString();
|
|
||||||
string sinceTimestampStr = @params["sinceTimestamp"]?.ToString(); // TODO: Implement timestamp filtering
|
|
||||||
string format = (@params["format"]?.ToString() ?? "detailed").ToLower();
|
|
||||||
bool includeStacktrace = @params["includeStacktrace"]?.ToObject<bool?>() ?? true;
|
|
||||||
|
|
||||||
if (types.Contains("all")) {
|
|
||||||
types = new List<string> { "error", "warning", "log" }; // Expand 'all'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(sinceTimestampStr))
|
|
||||||
{
|
|
||||||
Debug.LogWarning("[ReadConsole] Filtering by 'since_timestamp' is not currently implemented.");
|
|
||||||
// Need a way to get timestamp per log entry.
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetConsoleEntries(types, count, filterText, format, includeStacktrace);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Response.Error($"Unknown action: '{action}'. Valid actions are 'get' or 'clear'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.LogError($"[ReadConsole] Action '{action}' failed: {e}");
|
|
||||||
return Response.Error($"Internal error processing action '{action}': {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Action Implementations ---
|
|
||||||
|
|
||||||
private static object ClearConsole()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_clearMethod.Invoke(null, null); // Static method, no instance, no parameters
|
|
||||||
return Response.Success("Console cleared successfully.");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.LogError($"[ReadConsole] Failed to clear console: {e}");
|
|
||||||
return Response.Error($"Failed to clear console: {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object GetConsoleEntries(List<string> types, int? count, string filterText, string format, bool includeStacktrace)
|
|
||||||
{
|
|
||||||
List<object> formattedEntries = new List<object>();
|
|
||||||
int retrievedCount = 0;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// LogEntries requires calling Start/Stop around GetEntries/GetEntryInternal
|
|
||||||
_startGettingEntriesMethod.Invoke(null, null);
|
|
||||||
|
|
||||||
int totalEntries = (int)_getCountMethod.Invoke(null, null);
|
|
||||||
// Create instance to pass to GetEntryInternal - Ensure the type is correct
|
|
||||||
Type logEntryType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntry");
|
|
||||||
if (logEntryType == null) throw new Exception("Could not find internal type UnityEditor.LogEntry during GetConsoleEntries.");
|
|
||||||
object logEntryInstance = Activator.CreateInstance(logEntryType);
|
|
||||||
|
|
||||||
for (int i = 0; i < totalEntries; i++)
|
|
||||||
{
|
|
||||||
// Get the entry data into our instance using reflection
|
|
||||||
_getEntryMethod.Invoke(null, new object[] { i, logEntryInstance });
|
|
||||||
|
|
||||||
// Extract data using reflection
|
|
||||||
int mode = (int)_modeField.GetValue(logEntryInstance);
|
|
||||||
string message = (string)_messageField.GetValue(logEntryInstance);
|
|
||||||
string file = (string)_fileField.GetValue(logEntryInstance);
|
|
||||||
|
|
||||||
int line = (int)_lineField.GetValue(logEntryInstance);
|
|
||||||
// int instanceId = (int)_instanceIdField.GetValue(logEntryInstance);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(message)) continue; // Skip empty messages
|
|
||||||
|
|
||||||
// --- Filtering ---
|
|
||||||
// Filter by type
|
|
||||||
LogType currentType = GetLogTypeFromMode(mode);
|
|
||||||
if (!types.Contains(currentType.ToString().ToLowerInvariant()))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by text (case-insensitive)
|
|
||||||
if (!string.IsNullOrEmpty(filterText) && message.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) < 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Filter by timestamp (requires timestamp data)
|
|
||||||
|
|
||||||
// --- Formatting ---
|
|
||||||
string stackTrace = includeStacktrace ? ExtractStackTrace(message) : null;
|
|
||||||
// Get first line if stack is present and requested, otherwise use full message
|
|
||||||
string messageOnly = (includeStacktrace && !string.IsNullOrEmpty(stackTrace)) ? message.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)[0] : message;
|
|
||||||
|
|
||||||
object formattedEntry = null;
|
|
||||||
switch (format)
|
|
||||||
{
|
|
||||||
case "plain":
|
|
||||||
formattedEntry = messageOnly;
|
|
||||||
break;
|
|
||||||
case "json":
|
|
||||||
case "detailed": // Treat detailed as json for structured return
|
|
||||||
default:
|
|
||||||
formattedEntry = new {
|
|
||||||
type = currentType.ToString(),
|
|
||||||
message = messageOnly,
|
|
||||||
file = file,
|
|
||||||
line = line,
|
|
||||||
// timestamp = "", // TODO
|
|
||||||
stackTrace = stackTrace // Will be null if includeStacktrace is false or no stack found
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedEntries.Add(formattedEntry);
|
|
||||||
retrievedCount++;
|
|
||||||
|
|
||||||
// Apply count limit (after filtering)
|
|
||||||
if (count.HasValue && retrievedCount >= count.Value)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}");
|
|
||||||
// Ensure EndGettingEntries is called even if there's an error during iteration
|
|
||||||
try { _endGettingEntriesMethod.Invoke(null, null); } catch { /* Ignore nested exception */ }
|
|
||||||
return Response.Error($"Error retrieving log entries: {e.Message}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// Ensure we always call EndGettingEntries
|
|
||||||
try { _endGettingEntriesMethod.Invoke(null, null); } catch (Exception e) {
|
|
||||||
Debug.LogError($"[ReadConsole] Failed to call EndGettingEntries: {e}");
|
|
||||||
// Don't return error here as we might have valid data, but log it.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the filtered and formatted list (might be empty)
|
|
||||||
return Response.Success($"Retrieved {formattedEntries.Count} log entries.", formattedEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Internal Helpers ---
|
|
||||||
|
|
||||||
// Mapping from LogEntry.mode bits to LogType enum
|
|
||||||
// Based on decompiled UnityEditor code or common patterns. Precise bits might change between Unity versions.
|
|
||||||
// See comments below for LogEntry mode bits exploration.
|
|
||||||
// Note: This mapping is simplified and might not cover all edge cases or future Unity versions perfectly.
|
|
||||||
private const int ModeBitError = 1 << 0;
|
|
||||||
private const int ModeBitAssert = 1 << 1;
|
|
||||||
private const int ModeBitWarning = 1 << 2;
|
|
||||||
private const int ModeBitLog = 1 << 3;
|
|
||||||
private const int ModeBitException = 1 << 4; // Often combined with Error bits
|
|
||||||
private const int ModeBitScriptingError = 1 << 9;
|
|
||||||
private const int ModeBitScriptingWarning = 1 << 10;
|
|
||||||
private const int ModeBitScriptingLog = 1 << 11;
|
|
||||||
private const int ModeBitScriptingException = 1 << 18;
|
|
||||||
private const int ModeBitScriptingAssertion = 1 << 22;
|
|
||||||
|
|
||||||
|
|
||||||
private static LogType GetLogTypeFromMode(int mode)
|
|
||||||
{
|
|
||||||
// First, determine the type based on the original logic (most severe first)
|
|
||||||
LogType initialType;
|
|
||||||
if ((mode & (ModeBitError | ModeBitScriptingError | ModeBitException | ModeBitScriptingException)) != 0) {
|
|
||||||
initialType = LogType.Error;
|
|
||||||
}
|
|
||||||
else if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) {
|
|
||||||
initialType = LogType.Assert;
|
|
||||||
}
|
|
||||||
else if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) {
|
|
||||||
initialType = LogType.Warning;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
initialType = LogType.Log;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the observed "one level lower" correction
|
|
||||||
switch (initialType)
|
|
||||||
{
|
|
||||||
case LogType.Error: return LogType.Warning; // Error becomes Warning
|
|
||||||
case LogType.Warning: return LogType.Log; // Warning becomes Log
|
|
||||||
case LogType.Assert: return LogType.Assert; // Assert remains Assert (no lower level defined)
|
|
||||||
case LogType.Log: return LogType.Log; // Log remains Log
|
|
||||||
default: return LogType.Log; // Default fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to extract the stack trace part from a log message.
|
|
||||||
/// Unity log messages often have the stack trace appended after the main message,
|
|
||||||
/// starting on a new line and typically indented or beginning with "at ".
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fullMessage">The complete log message including potential stack trace.</param>
|
|
||||||
/// <returns>The extracted stack trace string, or null if none is found.</returns>
|
|
||||||
private static string ExtractStackTrace(string fullMessage)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(fullMessage)) return null;
|
|
||||||
|
|
||||||
// Split into lines, removing empty ones to handle different line endings gracefully.
|
|
||||||
// Using StringSplitOptions.None might be better if empty lines matter within stack trace, but RemoveEmptyEntries is usually safer here.
|
|
||||||
string[] lines = fullMessage.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
// If there's only one line or less, there's no separate stack trace.
|
|
||||||
if (lines.Length <= 1) return null;
|
|
||||||
|
|
||||||
int stackStartIndex = -1;
|
|
||||||
|
|
||||||
// Start checking from the second line onwards.
|
|
||||||
for(int i = 1; i < lines.Length; ++i)
|
|
||||||
{
|
|
||||||
// Performance: TrimStart creates a new string. Consider using IsWhiteSpace check if performance critical.
|
|
||||||
string trimmedLine = lines[i].TrimStart();
|
|
||||||
|
|
||||||
// Check for common stack trace patterns.
|
|
||||||
if (trimmedLine.StartsWith("at ") ||
|
|
||||||
trimmedLine.StartsWith("UnityEngine.") ||
|
|
||||||
trimmedLine.StartsWith("UnityEditor.") ||
|
|
||||||
trimmedLine.Contains("(at ") || // Covers "(at Assets/..." pattern
|
|
||||||
// Heuristic: Check if line starts with likely namespace/class pattern (Uppercase.Something)
|
|
||||||
(trimmedLine.Length > 0 && char.IsUpper(trimmedLine[0]) && trimmedLine.Contains('.'))
|
|
||||||
)
|
|
||||||
{
|
|
||||||
stackStartIndex = i;
|
|
||||||
break; // Found the likely start of the stack trace
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a potential start index was found...
|
|
||||||
if (stackStartIndex > 0)
|
|
||||||
{
|
|
||||||
// Join the lines from the stack start index onwards using standard newline characters.
|
|
||||||
// This reconstructs the stack trace part of the message.
|
|
||||||
return string.Join("\n", lines.Skip(stackStartIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
// No clear stack trace found based on the patterns.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* LogEntry.mode bits exploration (based on Unity decompilation/observation):
|
|
||||||
May change between versions.
|
|
||||||
|
|
||||||
Basic Types:
|
|
||||||
kError = 1 << 0 (1)
|
|
||||||
kAssert = 1 << 1 (2)
|
|
||||||
kWarning = 1 << 2 (4)
|
|
||||||
kLog = 1 << 3 (8)
|
|
||||||
kFatal = 1 << 4 (16) - Often treated as Exception/Error
|
|
||||||
|
|
||||||
Modifiers/Context:
|
|
||||||
kAssetImportError = 1 << 7 (128)
|
|
||||||
kAssetImportWarning = 1 << 8 (256)
|
|
||||||
kScriptingError = 1 << 9 (512)
|
|
||||||
kScriptingWarning = 1 << 10 (1024)
|
|
||||||
kScriptingLog = 1 << 11 (2048)
|
|
||||||
kScriptCompileError = 1 << 12 (4096)
|
|
||||||
kScriptCompileWarning = 1 << 13 (8192)
|
|
||||||
kStickyError = 1 << 14 (16384) - Stays visible even after Clear On Play
|
|
||||||
kMayIgnoreLineNumber = 1 << 15 (32768)
|
|
||||||
kReportBug = 1 << 16 (65536) - Shows the "Report Bug" button
|
|
||||||
kDisplayPreviousErrorInStatusBar = 1 << 17 (131072)
|
|
||||||
kScriptingException = 1 << 18 (262144)
|
|
||||||
kDontExtractStacktrace = 1 << 19 (524288) - Hint to the console UI
|
|
||||||
kShouldClearOnPlay = 1 << 20 (1048576) - Default behavior
|
|
||||||
kGraphCompileError = 1 << 21 (2097152)
|
|
||||||
kScriptingAssertion = 1 << 22 (4194304)
|
|
||||||
kVisualScriptingError = 1 << 23 (8388608)
|
|
||||||
|
|
||||||
Example observed values:
|
|
||||||
Log: 2048 (ScriptingLog) or 8 (Log)
|
|
||||||
Warning: 1028 (ScriptingWarning | Warning) or 4 (Warning)
|
|
||||||
Error: 513 (ScriptingError | Error) or 1 (Error)
|
|
||||||
Exception: 262161 (ScriptingException | Error | kFatal?) - Complex combination
|
|
||||||
Assertion: 4194306 (ScriptingAssertion | Assert) or 2 (Assert)
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: ea6797cf7f34d6044a89364e1ac4d4c9
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 41c60e5ab0e41d84ba997afc471ac58a
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 253b8f93cd23400478080cab9d619729
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: bbc163679c5bb0f418c6f6af1fa50f3a
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: ac3ad17989088c24598726ec3e0a53ba
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 872659ff7f5d9294ca6d47e93f6a111f
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 30b461704d14cea488b84870202ae45f
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 8865a8f86cc0a3240b94504bd2e5c0be
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 9230796797a49a54297a8fa444a1f5bb
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 67b82e49c36517040b7cfea8e421764e
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 27ffc6de0e9253e4f980ae545f07731a
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: a2f972b61922666418f99fa8f8ba817e
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: b34907e09ab90854fa849302b96c6247
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: be712c04494a1874593719eeb2a882ac
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: beb93a353b9140c44b7ac22d2bb8481a
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: c94ba17ca2284764f99d61356c5feded
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: c72711b644ecf0d40945ddba9b4bce77
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 714de9c710feb1a42878a16b7a4e7a6f
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 37728a13ca38f894b8760d808a909148
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 78cb4b2703910bc44b4dafad25cf8b35
|
guid: 31e7fac5858840340a75cc6df0ad3d9e
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using UnityMCP.Editor.Models;
|
using UnityMcpBridge.Editor.Models;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Data
|
namespace UnityMcpBridge.Editor.Data
|
||||||
{
|
{
|
||||||
public class DefaultServerConfig : ServerConfig
|
public class DefaultServerConfig : ServerConfig
|
||||||
{
|
{
|
||||||
|
|
@ -15,3 +15,4 @@ namespace UnityMCP.Editor.Data
|
||||||
public new float retryDelay = 1.0f;
|
public new float retryDelay = 1.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using UnityMCP.Editor.Models;
|
using UnityMcpBridge.Editor.Models;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Data
|
namespace UnityMcpBridge.Editor.Data
|
||||||
{
|
{
|
||||||
public class McpClients
|
public class McpClients
|
||||||
{
|
{
|
||||||
public List<McpClient> clients = new() {
|
public List<McpClient> clients = new()
|
||||||
new() {
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
name = "Claude Desktop",
|
name = "Claude Desktop",
|
||||||
windowsConfigPath = Path.Combine(
|
windowsConfigPath = Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
|
@ -23,9 +25,10 @@ namespace UnityMCP.Editor.Data
|
||||||
"claude_desktop_config.json"
|
"claude_desktop_config.json"
|
||||||
),
|
),
|
||||||
mcpType = McpTypes.ClaudeDesktop,
|
mcpType = McpTypes.ClaudeDesktop,
|
||||||
configStatus = "Not Configured"
|
configStatus = "Not Configured",
|
||||||
},
|
},
|
||||||
new() {
|
new()
|
||||||
|
{
|
||||||
name = "Cursor",
|
name = "Cursor",
|
||||||
windowsConfigPath = Path.Combine(
|
windowsConfigPath = Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
|
@ -38,8 +41,8 @@ namespace UnityMCP.Editor.Data
|
||||||
"mcp.json"
|
"mcp.json"
|
||||||
),
|
),
|
||||||
mcpType = McpTypes.Cursor,
|
mcpType = McpTypes.Cursor,
|
||||||
configStatus = "Not Configured"
|
configStatus = "Not Configured",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize status enums after construction
|
// Initialize status enums after construction
|
||||||
|
|
@ -55,3 +58,4 @@ namespace UnityMCP.Editor.Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Helpers
|
namespace UnityMcpBridge.Editor.Helpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides static methods for creating standardized success and error response objects.
|
/// Provides static methods for creating standardized success and error response objects.
|
||||||
|
|
@ -19,7 +19,12 @@ namespace UnityMCP.Editor.Helpers
|
||||||
{
|
{
|
||||||
if (data != null)
|
if (data != null)
|
||||||
{
|
{
|
||||||
return new { success = true, message = message, data = data };
|
return new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = message,
|
||||||
|
data = data,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -38,7 +43,12 @@ namespace UnityMCP.Editor.Helpers
|
||||||
if (data != null)
|
if (data != null)
|
||||||
{
|
{
|
||||||
// Note: The key is "error" for error messages, not "message"
|
// Note: The key is "error" for error messages, not "message"
|
||||||
return new { success = false, error = errorMessage, data = data };
|
return new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = errorMessage,
|
||||||
|
data = data,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -47,3 +57,4 @@ namespace UnityMCP.Editor.Helpers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using UnityEngine;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Helpers
|
namespace UnityMcpBridge.Editor.Helpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper class for Vector3 operations
|
/// Helper class for Vector3 operations
|
||||||
|
|
@ -22,3 +22,4 @@ namespace UnityMCP.Editor.Helpers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Models
|
namespace UnityMcpBridge.Editor.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a command received from the MCP client
|
/// Represents a command received from the MCP client
|
||||||
|
|
@ -18,3 +18,4 @@ namespace UnityMCP.Editor.Models
|
||||||
public JObject @params { get; set; }
|
public JObject @params { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Models
|
namespace UnityMcpBridge.Editor.Models
|
||||||
{
|
{
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class MCPConfigServer
|
public class McpConfigServer
|
||||||
{
|
{
|
||||||
[JsonProperty("command")]
|
[JsonProperty("command")]
|
||||||
public string command;
|
public string command;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace UnityMcpBridge.Editor.Models
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class McpConfigServers
|
||||||
|
{
|
||||||
|
[JsonProperty("unityMCP")]
|
||||||
|
public McpConfigServer unityMCP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace UnityMCP.Editor.Models
|
namespace UnityMcpBridge.Editor.Models
|
||||||
{
|
{
|
||||||
public class McpClient
|
public class McpClient
|
||||||
{
|
{
|
||||||
|
|
@ -24,7 +24,7 @@ namespace UnityMCP.Editor.Models
|
||||||
McpStatus.UnsupportedOS => "Unsupported OS",
|
McpStatus.UnsupportedOS => "Unsupported OS",
|
||||||
McpStatus.MissingConfig => "Missing UnityMCP Config",
|
McpStatus.MissingConfig => "Missing UnityMCP Config",
|
||||||
McpStatus.Error => configStatus.StartsWith("Error:") ? configStatus : "Error",
|
McpStatus.Error => configStatus.StartsWith("Error:") ? configStatus : "Error",
|
||||||
_ => "Unknown"
|
_ => "Unknown",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,5 +44,3 @@ namespace UnityMCP.Editor.Models
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Models
|
namespace UnityMcpBridge.Editor.Models
|
||||||
{
|
{
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class MCPConfig
|
public class McpConfig
|
||||||
{
|
{
|
||||||
[JsonProperty("mcpServers")]
|
[JsonProperty("mcpServers")]
|
||||||
public MCPConfigServers mcpServers;
|
public McpConfigServers mcpServers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
namespace UnityMcpBridge.Editor.Models
|
||||||
|
{
|
||||||
|
// Enum representing the various status states for MCP clients
|
||||||
|
public enum McpStatus
|
||||||
|
{
|
||||||
|
NotConfigured, // Not set up yet
|
||||||
|
Configured, // Successfully configured
|
||||||
|
Running, // Service is running
|
||||||
|
Connected, // Successfully connected
|
||||||
|
IncorrectPath, // Configuration has incorrect paths
|
||||||
|
CommunicationError, // Connected but communication issues
|
||||||
|
NoResponse, // Connected but not responding
|
||||||
|
MissingConfig, // Config file exists but missing required elements
|
||||||
|
UnsupportedOS, // OS is not supported
|
||||||
|
Error, // General error state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
namespace UnityMCP.Editor.Models
|
namespace UnityMcpBridge.Editor.Models
|
||||||
{
|
{
|
||||||
public enum McpTypes
|
public enum McpTypes
|
||||||
{
|
{
|
||||||
ClaudeDesktop,
|
ClaudeDesktop,
|
||||||
Cursor
|
Cursor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Models
|
namespace UnityMcpBridge.Editor.Models
|
||||||
{
|
{
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class ServerConfig
|
public class ServerConfig
|
||||||
|
|
@ -2,7 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Tools
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registry for all MCP command handlers (Refactored Version)
|
/// Registry for all MCP command handlers (Refactored Version)
|
||||||
|
|
@ -19,7 +19,7 @@ namespace UnityMCP.Editor.Tools
|
||||||
{ "HandleManageGameObject", ManageGameObject.HandleCommand },
|
{ "HandleManageGameObject", ManageGameObject.HandleCommand },
|
||||||
{ "HandleManageAsset", ManageAsset.HandleCommand },
|
{ "HandleManageAsset", ManageAsset.HandleCommand },
|
||||||
{ "HandleReadConsole", ReadConsole.HandleCommand },
|
{ "HandleReadConsole", ReadConsole.HandleCommand },
|
||||||
{ "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand }
|
{ "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -43,3 +43,4 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic; // Added for HashSet
|
using System.Collections.Generic; // Added for HashSet
|
||||||
using UnityMCP.Editor.Helpers; // For Response class
|
using Newtonsoft.Json.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityMcpBridge.Editor.Helpers; // For Response class
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Tools
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles executing Unity Editor menu items by path.
|
/// Handles executing Unity Editor menu items by path.
|
||||||
|
|
@ -14,7 +14,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
{
|
{
|
||||||
// Basic blacklist to prevent accidental execution of potentially disruptive menu items.
|
// Basic blacklist to prevent accidental execution of potentially disruptive menu items.
|
||||||
// This can be expanded based on needs.
|
// This can be expanded based on needs.
|
||||||
private static readonly HashSet<string> _menuPathBlacklist = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> _menuPathBlacklist = new HashSet<string>(
|
||||||
|
StringComparer.OrdinalIgnoreCase
|
||||||
|
)
|
||||||
{
|
{
|
||||||
"File/Quit",
|
"File/Quit",
|
||||||
// Add other potentially dangerous items like "Edit/Preferences...", "File/Build Settings..." if needed
|
// Add other potentially dangerous items like "Edit/Preferences...", "File/Build Settings..." if needed
|
||||||
|
|
@ -37,12 +39,19 @@ namespace UnityMCP.Editor.Tools
|
||||||
// Getting a comprehensive list of *all* menu items dynamically is very difficult
|
// Getting a comprehensive list of *all* menu items dynamically is very difficult
|
||||||
// and often requires complex reflection or maintaining a manual list.
|
// and often requires complex reflection or maintaining a manual list.
|
||||||
// Returning a placeholder/acknowledgement for now.
|
// Returning a placeholder/acknowledgement for now.
|
||||||
Debug.LogWarning("[ExecuteMenuItem] 'get_available_menus' action is not fully implemented. Dynamically listing all menu items is complex.");
|
Debug.LogWarning(
|
||||||
|
"[ExecuteMenuItem] 'get_available_menus' action is not fully implemented. Dynamically listing all menu items is complex."
|
||||||
|
);
|
||||||
// Returning an empty list as per the refactor plan's requirements.
|
// Returning an empty list as per the refactor plan's requirements.
|
||||||
return Response.Success("'get_available_menus' action is not fully implemented. Returning empty list.", new List<string>());
|
return Response.Success(
|
||||||
|
"'get_available_menus' action is not fully implemented. Returning empty list.",
|
||||||
|
new List<string>()
|
||||||
|
);
|
||||||
// TODO: Consider implementing a basic list of common/known menu items or exploring reflection techniques if this feature becomes critical.
|
// TODO: Consider implementing a basic list of common/known menu items or exploring reflection techniques if this feature becomes critical.
|
||||||
default:
|
default:
|
||||||
return Response.Error($"Unknown action: '{action}'. Valid actions are 'execute', 'get_available_menus'.");
|
return Response.Error(
|
||||||
|
$"Unknown action: '{action}'. Valid actions are 'execute', 'get_available_menus'."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -69,7 +78,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
// Validate against blacklist
|
// Validate against blacklist
|
||||||
if (_menuPathBlacklist.Contains(menuPath))
|
if (_menuPathBlacklist.Contains(menuPath))
|
||||||
{
|
{
|
||||||
return Response.Error($"Execution of menu item '{menuPath}' is blocked for safety reasons.");
|
return Response.Error(
|
||||||
|
$"Execution of menu item '{menuPath}' is blocked for safety reasons."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement alias lookup here if needed (Map alias to actual menuPath).
|
// TODO: Implement alias lookup here if needed (Map alias to actual menuPath).
|
||||||
|
|
@ -82,26 +93,41 @@ namespace UnityMCP.Editor.Tools
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Attempt to execute the menu item on the main thread using delayCall for safety.
|
// Attempt to execute the menu item on the main thread using delayCall for safety.
|
||||||
EditorApplication.delayCall += () => {
|
EditorApplication.delayCall += () =>
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
bool executed = EditorApplication.ExecuteMenuItem(menuPath);
|
bool executed = EditorApplication.ExecuteMenuItem(menuPath);
|
||||||
// Log potential failure inside the delayed call.
|
// Log potential failure inside the delayed call.
|
||||||
if (!executed) {
|
if (!executed)
|
||||||
Debug.LogError($"[ExecuteMenuItem] Failed to find or execute menu item via delayCall: '{menuPath}'. It might be invalid, disabled, or context-dependent.");
|
{
|
||||||
|
Debug.LogError(
|
||||||
|
$"[ExecuteMenuItem] Failed to find or execute menu item via delayCall: '{menuPath}'. It might be invalid, disabled, or context-dependent."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (Exception delayEx) {
|
}
|
||||||
Debug.LogError($"[ExecuteMenuItem] Exception during delayed execution of '{menuPath}': {delayEx}");
|
catch (Exception delayEx)
|
||||||
|
{
|
||||||
|
Debug.LogError(
|
||||||
|
$"[ExecuteMenuItem] Exception during delayed execution of '{menuPath}': {delayEx}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Report attempt immediately, as execution is delayed.
|
// Report attempt immediately, as execution is delayed.
|
||||||
return Response.Success($"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors.");
|
return Response.Success(
|
||||||
|
$"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
// Catch errors during setup phase.
|
// Catch errors during setup phase.
|
||||||
Debug.LogError($"[ExecuteMenuItem] Failed to setup execution for '{menuPath}': {e}");
|
Debug.LogError(
|
||||||
return Response.Error($"Error setting up execution for menu item '{menuPath}': {e.Message}");
|
$"[ExecuteMenuItem] Failed to setup execution for '{menuPath}': {e}"
|
||||||
|
);
|
||||||
|
return Response.Error(
|
||||||
|
$"Error setting up execution for menu item '{menuPath}': {e.Message}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,3 +135,4 @@ namespace UnityMCP.Editor.Tools
|
||||||
// private static string LookupAlias(string alias) { ... return actualMenuPath or null ... }
|
// private static string LookupAlias(string alias) { ... return actualMenuPath or null ... }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,15 +1,13 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityMCP.Editor.Helpers; // For Response class
|
using System.Linq;
|
||||||
using UnityEditor.ShortcutManagement;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using UnityEditor;
|
||||||
using UnityEditorInternal; // Required for tag management
|
using UnityEditorInternal; // Required for tag management
|
||||||
using System.Reflection; // Required for layer management
|
using UnityEngine;
|
||||||
|
using UnityMcpBridge.Editor.Helpers; // For Response class
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Tools
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles operations related to controlling and querying the Unity Editor state,
|
/// Handles operations related to controlling and querying the Unity Editor state,
|
||||||
|
|
@ -19,6 +17,7 @@ namespace UnityMCP.Editor.Tools
|
||||||
{
|
{
|
||||||
// Constant for starting user layer index
|
// Constant for starting user layer index
|
||||||
private const int FirstUserLayerIndex = 8;
|
private const int FirstUserLayerIndex = 8;
|
||||||
|
|
||||||
// Constant for total layer count
|
// Constant for total layer count
|
||||||
private const int TotalLayerCount = 32;
|
private const int TotalLayerCount = 32;
|
||||||
|
|
||||||
|
|
@ -62,7 +61,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
if (EditorApplication.isPlaying)
|
if (EditorApplication.isPlaying)
|
||||||
{
|
{
|
||||||
EditorApplication.isPaused = !EditorApplication.isPaused;
|
EditorApplication.isPaused = !EditorApplication.isPaused;
|
||||||
return Response.Success(EditorApplication.isPaused ? "Game paused." : "Game resumed.");
|
return Response.Success(
|
||||||
|
EditorApplication.isPaused ? "Game paused." : "Game resumed."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Response.Error("Cannot pause/resume: Not in play mode.");
|
return Response.Error("Cannot pause/resume: Not in play mode.");
|
||||||
}
|
}
|
||||||
|
|
@ -96,25 +97,30 @@ namespace UnityMCP.Editor.Tools
|
||||||
return GetSelection();
|
return GetSelection();
|
||||||
case "set_active_tool":
|
case "set_active_tool":
|
||||||
string toolName = @params["toolName"]?.ToString();
|
string toolName = @params["toolName"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(toolName)) return Response.Error("'toolName' parameter required for set_active_tool.");
|
if (string.IsNullOrEmpty(toolName))
|
||||||
|
return Response.Error("'toolName' parameter required for set_active_tool.");
|
||||||
return SetActiveTool(toolName);
|
return SetActiveTool(toolName);
|
||||||
|
|
||||||
// Tag Management
|
// Tag Management
|
||||||
case "add_tag":
|
case "add_tag":
|
||||||
if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for add_tag.");
|
if (string.IsNullOrEmpty(tagName))
|
||||||
|
return Response.Error("'tagName' parameter required for add_tag.");
|
||||||
return AddTag(tagName);
|
return AddTag(tagName);
|
||||||
case "remove_tag":
|
case "remove_tag":
|
||||||
if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for remove_tag.");
|
if (string.IsNullOrEmpty(tagName))
|
||||||
|
return Response.Error("'tagName' parameter required for remove_tag.");
|
||||||
return RemoveTag(tagName);
|
return RemoveTag(tagName);
|
||||||
case "get_tags":
|
case "get_tags":
|
||||||
return GetTags(); // Helper to list current tags
|
return GetTags(); // Helper to list current tags
|
||||||
|
|
||||||
// Layer Management
|
// Layer Management
|
||||||
case "add_layer":
|
case "add_layer":
|
||||||
if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for add_layer.");
|
if (string.IsNullOrEmpty(layerName))
|
||||||
|
return Response.Error("'layerName' parameter required for add_layer.");
|
||||||
return AddLayer(layerName);
|
return AddLayer(layerName);
|
||||||
case "remove_layer":
|
case "remove_layer":
|
||||||
if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for remove_layer.");
|
if (string.IsNullOrEmpty(layerName))
|
||||||
|
return Response.Error("'layerName' parameter required for remove_layer.");
|
||||||
return RemoveLayer(layerName);
|
return RemoveLayer(layerName);
|
||||||
case "get_layers":
|
case "get_layers":
|
||||||
return GetLayers(); // Helper to list current layers
|
return GetLayers(); // Helper to list current layers
|
||||||
|
|
@ -130,7 +136,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
// return SetQualityLevel(@params["qualityLevel"]);
|
// return SetQualityLevel(@params["qualityLevel"]);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Response.Error($"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_windows, get_active_tool, get_selection, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers.");
|
return Response.Error(
|
||||||
|
$"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_windows, get_active_tool, get_selection, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +155,7 @@ namespace UnityMCP.Editor.Tools
|
||||||
isUpdating = EditorApplication.isUpdating,
|
isUpdating = EditorApplication.isUpdating,
|
||||||
applicationPath = EditorApplication.applicationPath,
|
applicationPath = EditorApplication.applicationPath,
|
||||||
applicationContentsPath = EditorApplication.applicationContentsPath,
|
applicationContentsPath = EditorApplication.applicationContentsPath,
|
||||||
timeSinceStartup = EditorApplication.timeSinceStartup
|
timeSinceStartup = EditorApplication.timeSinceStartup,
|
||||||
};
|
};
|
||||||
return Response.Success("Retrieved editor state.", state);
|
return Response.Success("Retrieved editor state.", state);
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +170,8 @@ namespace UnityMCP.Editor.Tools
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get all types deriving from EditorWindow
|
// Get all types deriving from EditorWindow
|
||||||
var windowTypes = AppDomain.CurrentDomain.GetAssemblies()
|
var windowTypes = AppDomain
|
||||||
|
.CurrentDomain.GetAssemblies()
|
||||||
.SelectMany(assembly => assembly.GetTypes())
|
.SelectMany(assembly => assembly.GetTypes())
|
||||||
.Where(type => type.IsSubclassOf(typeof(EditorWindow)))
|
.Where(type => type.IsSubclassOf(typeof(EditorWindow)))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
@ -175,22 +184,33 @@ namespace UnityMCP.Editor.Tools
|
||||||
|
|
||||||
foreach (EditorWindow window in allWindows)
|
foreach (EditorWindow window in allWindows)
|
||||||
{
|
{
|
||||||
if (window == null) continue; // Skip potentially destroyed windows
|
if (window == null)
|
||||||
|
continue; // Skip potentially destroyed windows
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
openWindows.Add(new
|
openWindows.Add(
|
||||||
|
new
|
||||||
{
|
{
|
||||||
title = window.titleContent.text,
|
title = window.titleContent.text,
|
||||||
typeName = window.GetType().FullName,
|
typeName = window.GetType().FullName,
|
||||||
isFocused = EditorWindow.focusedWindow == window,
|
isFocused = EditorWindow.focusedWindow == window,
|
||||||
position = new { x = window.position.x, y = window.position.y, width = window.position.width, height = window.position.height },
|
position = new
|
||||||
instanceID = window.GetInstanceID()
|
{
|
||||||
});
|
x = window.position.x,
|
||||||
|
y = window.position.y,
|
||||||
|
width = window.position.width,
|
||||||
|
height = window.position.height,
|
||||||
|
},
|
||||||
|
instanceID = window.GetInstanceID(),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"Could not get info for window {window.GetType().Name}: {ex.Message}");
|
Debug.LogWarning(
|
||||||
|
$"Could not get info for window {window.GetType().Name}: {ex.Message}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,15 +229,18 @@ namespace UnityMCP.Editor.Tools
|
||||||
Tool currentTool = UnityEditor.Tools.current;
|
Tool currentTool = UnityEditor.Tools.current;
|
||||||
string toolName = currentTool.ToString(); // Enum to string
|
string toolName = currentTool.ToString(); // Enum to string
|
||||||
bool customToolActive = UnityEditor.Tools.current == Tool.Custom; // Check if a custom tool is active
|
bool customToolActive = UnityEditor.Tools.current == Tool.Custom; // Check if a custom tool is active
|
||||||
string activeToolName = customToolActive ? EditorTools.GetActiveToolName() : toolName; // Get custom name if needed
|
string activeToolName = customToolActive
|
||||||
|
? EditorTools.GetActiveToolName()
|
||||||
|
: toolName; // Get custom name if needed
|
||||||
|
|
||||||
var toolInfo = new {
|
var toolInfo = new
|
||||||
|
{
|
||||||
activeTool = activeToolName,
|
activeTool = activeToolName,
|
||||||
isCustom = customToolActive,
|
isCustom = customToolActive,
|
||||||
pivotMode = UnityEditor.Tools.pivotMode.ToString(),
|
pivotMode = UnityEditor.Tools.pivotMode.ToString(),
|
||||||
pivotRotation = UnityEditor.Tools.pivotRotation.ToString(),
|
pivotRotation = UnityEditor.Tools.pivotRotation.ToString(),
|
||||||
handleRotation = UnityEditor.Tools.handleRotation.eulerAngles, // Euler for simplicity
|
handleRotation = UnityEditor.Tools.handleRotation.eulerAngles, // Euler for simplicity
|
||||||
handlePosition = UnityEditor.Tools.handlePosition
|
handlePosition = UnityEditor.Tools.handlePosition,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.Success("Retrieved active tool information.", toolInfo);
|
return Response.Success("Retrieved active tool information.", toolInfo);
|
||||||
|
|
@ -243,14 +266,18 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return Response.Error($"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid.");
|
return Response.Error(
|
||||||
|
$"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Potentially try activating a custom tool by name here if needed
|
// Potentially try activating a custom tool by name here if needed
|
||||||
// This often requires specific editor scripting knowledge for that tool.
|
// This often requires specific editor scripting knowledge for that tool.
|
||||||
return Response.Error($"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom).");
|
return Response.Error(
|
||||||
|
$"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom)."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -270,9 +297,22 @@ namespace UnityMCP.Editor.Tools
|
||||||
activeTransform = Selection.activeTransform?.name,
|
activeTransform = Selection.activeTransform?.name,
|
||||||
activeInstanceID = Selection.activeInstanceID,
|
activeInstanceID = Selection.activeInstanceID,
|
||||||
count = Selection.count,
|
count = Selection.count,
|
||||||
objects = Selection.objects.Select(obj => new { name = obj?.name, type = obj?.GetType().FullName, instanceID = obj?.GetInstanceID() }).ToList(),
|
objects = Selection
|
||||||
gameObjects = Selection.gameObjects.Select(go => new { name = go?.name, instanceID = go?.GetInstanceID() }).ToList(),
|
.objects.Select(obj => new
|
||||||
assetGUIDs = Selection.assetGUIDs // GUIDs for selected assets in Project view
|
{
|
||||||
|
name = obj?.name,
|
||||||
|
type = obj?.GetType().FullName,
|
||||||
|
instanceID = obj?.GetInstanceID(),
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
gameObjects = Selection
|
||||||
|
.gameObjects.Select(go => new
|
||||||
|
{
|
||||||
|
name = go?.name,
|
||||||
|
instanceID = go?.GetInstanceID(),
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
assetGUIDs = Selection.assetGUIDs, // GUIDs for selected assets in Project view
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.Success("Retrieved current selection details.", selectionInfo);
|
return Response.Success("Retrieved current selection details.", selectionInfo);
|
||||||
|
|
@ -351,7 +391,6 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Layer Management Methods ---
|
// --- Layer Management Methods ---
|
||||||
|
|
||||||
private static object AddLayer(string layerName)
|
private static object AddLayer(string layerName)
|
||||||
|
|
@ -361,7 +400,8 @@ namespace UnityMCP.Editor.Tools
|
||||||
|
|
||||||
// Access the TagManager asset
|
// Access the TagManager asset
|
||||||
SerializedObject tagManager = GetTagManager();
|
SerializedObject tagManager = GetTagManager();
|
||||||
if (tagManager == null) return Response.Error("Could not access TagManager asset.");
|
if (tagManager == null)
|
||||||
|
return Response.Error("Could not access TagManager asset.");
|
||||||
|
|
||||||
SerializedProperty layersProp = tagManager.FindProperty("layers");
|
SerializedProperty layersProp = tagManager.FindProperty("layers");
|
||||||
if (layersProp == null || !layersProp.isArray)
|
if (layersProp == null || !layersProp.isArray)
|
||||||
|
|
@ -371,7 +411,10 @@ namespace UnityMCP.Editor.Tools
|
||||||
for (int i = 0; i < TotalLayerCount; i++)
|
for (int i = 0; i < TotalLayerCount; i++)
|
||||||
{
|
{
|
||||||
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
|
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
|
||||||
if (layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase))
|
if (
|
||||||
|
layerSP != null
|
||||||
|
&& layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return Response.Error($"Layer '{layerName}' already exists at index {i}.");
|
return Response.Error($"Layer '{layerName}' already exists at index {i}.");
|
||||||
}
|
}
|
||||||
|
|
@ -397,13 +440,17 @@ namespace UnityMCP.Editor.Tools
|
||||||
// Assign the name to the found slot
|
// Assign the name to the found slot
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(firstEmptyUserLayer);
|
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(
|
||||||
|
firstEmptyUserLayer
|
||||||
|
);
|
||||||
targetLayerSP.stringValue = layerName;
|
targetLayerSP.stringValue = layerName;
|
||||||
// Apply the changes to the TagManager asset
|
// Apply the changes to the TagManager asset
|
||||||
tagManager.ApplyModifiedProperties();
|
tagManager.ApplyModifiedProperties();
|
||||||
// Save assets to make sure it's written to disk
|
// Save assets to make sure it's written to disk
|
||||||
AssetDatabase.SaveAssets();
|
AssetDatabase.SaveAssets();
|
||||||
return Response.Success($"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}.");
|
return Response.Success(
|
||||||
|
$"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -418,7 +465,8 @@ namespace UnityMCP.Editor.Tools
|
||||||
|
|
||||||
// Access the TagManager asset
|
// Access the TagManager asset
|
||||||
SerializedObject tagManager = GetTagManager();
|
SerializedObject tagManager = GetTagManager();
|
||||||
if (tagManager == null) return Response.Error("Could not access TagManager asset.");
|
if (tagManager == null)
|
||||||
|
return Response.Error("Could not access TagManager asset.");
|
||||||
|
|
||||||
SerializedProperty layersProp = tagManager.FindProperty("layers");
|
SerializedProperty layersProp = tagManager.FindProperty("layers");
|
||||||
if (layersProp == null || !layersProp.isArray)
|
if (layersProp == null || !layersProp.isArray)
|
||||||
|
|
@ -430,7 +478,10 @@ namespace UnityMCP.Editor.Tools
|
||||||
{
|
{
|
||||||
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
|
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
|
||||||
// Case-insensitive comparison is safer
|
// Case-insensitive comparison is safer
|
||||||
if (layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase))
|
if (
|
||||||
|
layerSP != null
|
||||||
|
&& layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
layerIndexToRemove = i;
|
layerIndexToRemove = i;
|
||||||
break;
|
break;
|
||||||
|
|
@ -445,13 +496,17 @@ namespace UnityMCP.Editor.Tools
|
||||||
// Clear the name for that index
|
// Clear the name for that index
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(layerIndexToRemove);
|
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(
|
||||||
|
layerIndexToRemove
|
||||||
|
);
|
||||||
targetLayerSP.stringValue = string.Empty; // Set to empty string to remove
|
targetLayerSP.stringValue = string.Empty; // Set to empty string to remove
|
||||||
// Apply the changes
|
// Apply the changes
|
||||||
tagManager.ApplyModifiedProperties();
|
tagManager.ApplyModifiedProperties();
|
||||||
// Save assets
|
// Save assets
|
||||||
AssetDatabase.SaveAssets();
|
AssetDatabase.SaveAssets();
|
||||||
return Response.Success($"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully.");
|
return Response.Success(
|
||||||
|
$"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -480,7 +535,6 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Helper Methods ---
|
// --- Helper Methods ---
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -491,7 +545,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Load the TagManager asset from the ProjectSettings folder
|
// Load the TagManager asset from the ProjectSettings folder
|
||||||
UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/TagManager.asset");
|
UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath(
|
||||||
|
"ProjectSettings/TagManager.asset"
|
||||||
|
);
|
||||||
if (tagManagerAssets == null || tagManagerAssets.Length == 0)
|
if (tagManagerAssets == null || tagManagerAssets.Length == 0)
|
||||||
{
|
{
|
||||||
Debug.LogError("[ManageEditor] TagManager.asset not found in ProjectSettings.");
|
Debug.LogError("[ManageEditor] TagManager.asset not found in ProjectSettings.");
|
||||||
|
|
@ -515,8 +571,10 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper class to get custom tool names (remains the same)
|
// Helper class to get custom tool names (remains the same)
|
||||||
internal static class EditorTools {
|
internal static class EditorTools
|
||||||
public static string GetActiveToolName() {
|
{
|
||||||
|
public static string GetActiveToolName()
|
||||||
|
{
|
||||||
// This is a placeholder. Real implementation depends on how custom tools
|
// This is a placeholder. Real implementation depends on how custom tools
|
||||||
// are registered and tracked in the specific Unity project setup.
|
// are registered and tracked in the specific Unity project setup.
|
||||||
// It might involve checking static variables, calling methods on specific tool managers, etc.
|
// It might involve checking static variables, calling methods on specific tool managers, etc.
|
||||||
|
|
@ -530,3 +588,4 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,15 +1,15 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.SceneManagement;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.SceneManagement;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using Newtonsoft.Json.Linq;
|
||||||
using UnityMCP.Editor.Helpers; // For Response class
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
using UnityMcpBridge.Editor.Helpers; // For Response class
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Tools
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles scene management operations like loading, saving, creating, and querying hierarchy.
|
/// Handles scene management operations like loading, saving, creating, and querying hierarchy.
|
||||||
|
|
@ -29,7 +29,8 @@ namespace UnityMCP.Editor.Tools
|
||||||
|
|
||||||
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
||||||
string relativeDir = path ?? string.Empty;
|
string relativeDir = path ?? string.Empty;
|
||||||
if (!string.IsNullOrEmpty(relativeDir)) {
|
if (!string.IsNullOrEmpty(relativeDir))
|
||||||
|
{
|
||||||
relativeDir = relativeDir.Replace('\\', '/').Trim('/');
|
relativeDir = relativeDir.Replace('\\', '/').Trim('/');
|
||||||
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
@ -51,9 +52,13 @@ namespace UnityMCP.Editor.Tools
|
||||||
string sceneFileName = string.IsNullOrEmpty(name) ? null : $"{name}.unity";
|
string sceneFileName = string.IsNullOrEmpty(name) ? null : $"{name}.unity";
|
||||||
// Construct full system path correctly: ProjectRoot/Assets/relativeDir/sceneFileName
|
// Construct full system path correctly: ProjectRoot/Assets/relativeDir/sceneFileName
|
||||||
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Combine with Assets path (Application.dataPath ends in Assets)
|
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Combine with Assets path (Application.dataPath ends in Assets)
|
||||||
string fullPath = string.IsNullOrEmpty(sceneFileName) ? null : Path.Combine(fullPathDir, sceneFileName);
|
string fullPath = string.IsNullOrEmpty(sceneFileName)
|
||||||
|
? null
|
||||||
|
: Path.Combine(fullPathDir, sceneFileName);
|
||||||
// Ensure relativePath always starts with "Assets/" and uses forward slashes
|
// Ensure relativePath always starts with "Assets/" and uses forward slashes
|
||||||
string relativePath = string.IsNullOrEmpty(sceneFileName) ? null : Path.Combine("Assets", relativeDir, sceneFileName).Replace('\\', '/');
|
string relativePath = string.IsNullOrEmpty(sceneFileName)
|
||||||
|
? null
|
||||||
|
: Path.Combine("Assets", relativeDir, sceneFileName).Replace('\\', '/');
|
||||||
|
|
||||||
// Ensure directory exists for 'create'
|
// Ensure directory exists for 'create'
|
||||||
if (action == "create" && !string.IsNullOrEmpty(fullPathDir))
|
if (action == "create" && !string.IsNullOrEmpty(fullPathDir))
|
||||||
|
|
@ -64,7 +69,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return Response.Error($"Could not create directory '{fullPathDir}': {e.Message}");
|
return Response.Error(
|
||||||
|
$"Could not create directory '{fullPathDir}': {e.Message}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,7 +80,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
{
|
{
|
||||||
case "create":
|
case "create":
|
||||||
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(relativePath))
|
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(relativePath))
|
||||||
return Response.Error("'name' and 'path' parameters are required for 'create' action.");
|
return Response.Error(
|
||||||
|
"'name' and 'path' parameters are required for 'create' action."
|
||||||
|
);
|
||||||
return CreateScene(fullPath, relativePath);
|
return CreateScene(fullPath, relativePath);
|
||||||
case "load":
|
case "load":
|
||||||
// Loading can be done by path/name or build index
|
// Loading can be done by path/name or build index
|
||||||
|
|
@ -82,7 +91,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
else if (buildIndex.HasValue)
|
else if (buildIndex.HasValue)
|
||||||
return LoadScene(buildIndex.Value);
|
return LoadScene(buildIndex.Value);
|
||||||
else
|
else
|
||||||
return Response.Error("Either 'name'/'path' or 'buildIndex' must be provided for 'load' action.");
|
return Response.Error(
|
||||||
|
"Either 'name'/'path' or 'buildIndex' must be provided for 'load' action."
|
||||||
|
);
|
||||||
case "save":
|
case "save":
|
||||||
// Save current scene, optionally to a new path
|
// Save current scene, optionally to a new path
|
||||||
return SaveScene(fullPath, relativePath);
|
return SaveScene(fullPath, relativePath);
|
||||||
|
|
@ -94,7 +105,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
return GetBuildSettingsScenes();
|
return GetBuildSettingsScenes();
|
||||||
// Add cases for modifying build settings, additive loading, unloading etc.
|
// Add cases for modifying build settings, additive loading, unloading etc.
|
||||||
default:
|
default:
|
||||||
return Response.Error($"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings.");
|
return Response.Error(
|
||||||
|
$"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,14 +121,20 @@ namespace UnityMCP.Editor.Tools
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Create a new empty scene
|
// Create a new empty scene
|
||||||
Scene newScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
Scene newScene = EditorSceneManager.NewScene(
|
||||||
|
NewSceneSetup.EmptyScene,
|
||||||
|
NewSceneMode.Single
|
||||||
|
);
|
||||||
// Save it to the specified path
|
// Save it to the specified path
|
||||||
bool saved = EditorSceneManager.SaveScene(newScene, relativePath);
|
bool saved = EditorSceneManager.SaveScene(newScene, relativePath);
|
||||||
|
|
||||||
if (saved)
|
if (saved)
|
||||||
{
|
{
|
||||||
AssetDatabase.Refresh(); // Ensure Unity sees the new scene file
|
AssetDatabase.Refresh(); // Ensure Unity sees the new scene file
|
||||||
return Response.Success($"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.", new { path = relativePath });
|
return Response.Success(
|
||||||
|
$"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.",
|
||||||
|
new { path = relativePath }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -132,7 +151,17 @@ namespace UnityMCP.Editor.Tools
|
||||||
|
|
||||||
private static object LoadScene(string relativePath)
|
private static object LoadScene(string relativePath)
|
||||||
{
|
{
|
||||||
if (!File.Exists(Path.Combine(Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length), relativePath)))
|
if (
|
||||||
|
!File.Exists(
|
||||||
|
Path.Combine(
|
||||||
|
Application.dataPath.Substring(
|
||||||
|
0,
|
||||||
|
Application.dataPath.Length - "Assets".Length
|
||||||
|
),
|
||||||
|
relativePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return Response.Error($"Scene file not found at '{relativePath}'.");
|
return Response.Error($"Scene file not found at '{relativePath}'.");
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +170,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
if (EditorSceneManager.GetActiveScene().isDirty)
|
if (EditorSceneManager.GetActiveScene().isDirty)
|
||||||
{
|
{
|
||||||
// Optionally prompt the user or save automatically before loading
|
// Optionally prompt the user or save automatically before loading
|
||||||
return Response.Error("Current scene has unsaved changes. Please save or discard changes before loading a new scene.");
|
return Response.Error(
|
||||||
|
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
|
||||||
|
);
|
||||||
// Example: bool saveOK = EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
|
// Example: bool saveOK = EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
|
||||||
// if (!saveOK) return Response.Error("Load cancelled by user.");
|
// if (!saveOK) return Response.Error("Load cancelled by user.");
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +180,14 @@ namespace UnityMCP.Editor.Tools
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
EditorSceneManager.OpenScene(relativePath, OpenSceneMode.Single);
|
EditorSceneManager.OpenScene(relativePath, OpenSceneMode.Single);
|
||||||
return Response.Success($"Scene '{relativePath}' loaded successfully.", new { path = relativePath, name = Path.GetFileNameWithoutExtension(relativePath) });
|
return Response.Success(
|
||||||
|
$"Scene '{relativePath}' loaded successfully.",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
path = relativePath,
|
||||||
|
name = Path.GetFileNameWithoutExtension(relativePath),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -161,24 +199,38 @@ namespace UnityMCP.Editor.Tools
|
||||||
{
|
{
|
||||||
if (buildIndex < 0 || buildIndex >= SceneManager.sceneCountInBuildSettings)
|
if (buildIndex < 0 || buildIndex >= SceneManager.sceneCountInBuildSettings)
|
||||||
{
|
{
|
||||||
return Response.Error($"Invalid build index: {buildIndex}. Must be between 0 and {SceneManager.sceneCountInBuildSettings - 1}.");
|
return Response.Error(
|
||||||
|
$"Invalid build index: {buildIndex}. Must be between 0 and {SceneManager.sceneCountInBuildSettings - 1}."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for unsaved changes
|
// Check for unsaved changes
|
||||||
if (EditorSceneManager.GetActiveScene().isDirty)
|
if (EditorSceneManager.GetActiveScene().isDirty)
|
||||||
{
|
{
|
||||||
return Response.Error("Current scene has unsaved changes. Please save or discard changes before loading a new scene.");
|
return Response.Error(
|
||||||
|
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex);
|
string scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex);
|
||||||
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
|
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
|
||||||
return Response.Success($"Scene at build index {buildIndex} ('{scenePath}') loaded successfully.", new { path = scenePath, name = Path.GetFileNameWithoutExtension(scenePath), buildIndex = buildIndex });
|
return Response.Success(
|
||||||
|
$"Scene at build index {buildIndex} ('{scenePath}') loaded successfully.",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
path = scenePath,
|
||||||
|
name = Path.GetFileNameWithoutExtension(scenePath),
|
||||||
|
buildIndex = buildIndex,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return Response.Error($"Error loading scene with build index {buildIndex}: {e.Message}");
|
return Response.Error(
|
||||||
|
$"Error loading scene with build index {buildIndex}: {e.Message}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,7 +252,8 @@ namespace UnityMCP.Editor.Tools
|
||||||
// Save As...
|
// Save As...
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
string dir = Path.GetDirectoryName(fullPath);
|
string dir = Path.GetDirectoryName(fullPath);
|
||||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
if (!Directory.Exists(dir))
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
saved = EditorSceneManager.SaveScene(currentScene, relativePath);
|
saved = EditorSceneManager.SaveScene(currentScene, relativePath);
|
||||||
finalPath = relativePath;
|
finalPath = relativePath;
|
||||||
|
|
@ -211,7 +264,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
if (string.IsNullOrEmpty(currentScene.path))
|
if (string.IsNullOrEmpty(currentScene.path))
|
||||||
{
|
{
|
||||||
// Scene is untitled, needs a path
|
// Scene is untitled, needs a path
|
||||||
return Response.Error("Cannot save an untitled scene without providing a 'name' and 'path'. Use Save As functionality.");
|
return Response.Error(
|
||||||
|
"Cannot save an untitled scene without providing a 'name' and 'path'. Use Save As functionality."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
saved = EditorSceneManager.SaveScene(currentScene);
|
saved = EditorSceneManager.SaveScene(currentScene);
|
||||||
}
|
}
|
||||||
|
|
@ -219,7 +274,10 @@ namespace UnityMCP.Editor.Tools
|
||||||
if (saved)
|
if (saved)
|
||||||
{
|
{
|
||||||
AssetDatabase.Refresh();
|
AssetDatabase.Refresh();
|
||||||
return Response.Success($"Scene '{currentScene.name}' saved successfully to '{finalPath}'.", new { path = finalPath, name = currentScene.name });
|
return Response.Success(
|
||||||
|
$"Scene '{currentScene.name}' saved successfully to '{finalPath}'.",
|
||||||
|
new { path = finalPath, name = currentScene.name }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -249,7 +307,7 @@ namespace UnityMCP.Editor.Tools
|
||||||
buildIndex = activeScene.buildIndex, // -1 if not in build settings
|
buildIndex = activeScene.buildIndex, // -1 if not in build settings
|
||||||
isDirty = activeScene.isDirty,
|
isDirty = activeScene.isDirty,
|
||||||
isLoaded = activeScene.isLoaded,
|
isLoaded = activeScene.isLoaded,
|
||||||
rootCount = activeScene.rootCount
|
rootCount = activeScene.rootCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.Success("Retrieved active scene information.", sceneInfo);
|
return Response.Success("Retrieved active scene information.", sceneInfo);
|
||||||
|
|
@ -268,12 +326,15 @@ namespace UnityMCP.Editor.Tools
|
||||||
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
|
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
|
||||||
{
|
{
|
||||||
var scene = EditorBuildSettings.scenes[i];
|
var scene = EditorBuildSettings.scenes[i];
|
||||||
scenes.Add(new {
|
scenes.Add(
|
||||||
|
new
|
||||||
|
{
|
||||||
path = scene.path,
|
path = scene.path,
|
||||||
guid = scene.guid.ToString(),
|
guid = scene.guid.ToString(),
|
||||||
enabled = scene.enabled,
|
enabled = scene.enabled,
|
||||||
buildIndex = i // Actual build index considering only enabled scenes might differ
|
buildIndex = i, // Actual build index considering only enabled scenes might differ
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Response.Success("Retrieved scenes from Build Settings.", scenes);
|
return Response.Success("Retrieved scenes from Build Settings.", scenes);
|
||||||
}
|
}
|
||||||
|
|
@ -290,13 +351,18 @@ namespace UnityMCP.Editor.Tools
|
||||||
Scene activeScene = EditorSceneManager.GetActiveScene();
|
Scene activeScene = EditorSceneManager.GetActiveScene();
|
||||||
if (!activeScene.IsValid() || !activeScene.isLoaded)
|
if (!activeScene.IsValid() || !activeScene.isLoaded)
|
||||||
{
|
{
|
||||||
return Response.Error("No valid and loaded scene is active to get hierarchy from.");
|
return Response.Error(
|
||||||
|
"No valid and loaded scene is active to get hierarchy from."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
GameObject[] rootObjects = activeScene.GetRootGameObjects();
|
GameObject[] rootObjects = activeScene.GetRootGameObjects();
|
||||||
var hierarchy = rootObjects.Select(go => GetGameObjectDataRecursive(go)).ToList();
|
var hierarchy = rootObjects.Select(go => GetGameObjectDataRecursive(go)).ToList();
|
||||||
|
|
||||||
return Response.Success($"Retrieved hierarchy for scene '{activeScene.name}'.", hierarchy);
|
return Response.Success(
|
||||||
|
$"Retrieved hierarchy for scene '{activeScene.name}'.",
|
||||||
|
hierarchy
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -309,7 +375,8 @@ namespace UnityMCP.Editor.Tools
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static object GetGameObjectDataRecursive(GameObject go)
|
private static object GetGameObjectDataRecursive(GameObject go)
|
||||||
{
|
{
|
||||||
if (go == null) return null;
|
if (go == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
var childrenData = new List<object>();
|
var childrenData = new List<object>();
|
||||||
foreach (Transform child in go.transform)
|
foreach (Transform child in go.transform)
|
||||||
|
|
@ -326,16 +393,35 @@ namespace UnityMCP.Editor.Tools
|
||||||
{ "layer", go.layer },
|
{ "layer", go.layer },
|
||||||
{ "isStatic", go.isStatic },
|
{ "isStatic", go.isStatic },
|
||||||
{ "instanceID", go.GetInstanceID() }, // Useful unique identifier
|
{ "instanceID", go.GetInstanceID() }, // Useful unique identifier
|
||||||
{ "transform", new {
|
{
|
||||||
position = new { x = go.transform.localPosition.x, y = go.transform.localPosition.y, z = go.transform.localPosition.z },
|
"transform",
|
||||||
rotation = new { x = go.transform.localRotation.eulerAngles.x, y = go.transform.localRotation.eulerAngles.y, z = go.transform.localRotation.eulerAngles.z }, // Euler for simplicity
|
new
|
||||||
scale = new { x = go.transform.localScale.x, y = go.transform.localScale.y, z = go.transform.localScale.z }
|
{
|
||||||
|
position = new
|
||||||
|
{
|
||||||
|
x = go.transform.localPosition.x,
|
||||||
|
y = go.transform.localPosition.y,
|
||||||
|
z = go.transform.localPosition.z,
|
||||||
|
},
|
||||||
|
rotation = new
|
||||||
|
{
|
||||||
|
x = go.transform.localRotation.eulerAngles.x,
|
||||||
|
y = go.transform.localRotation.eulerAngles.y,
|
||||||
|
z = go.transform.localRotation.eulerAngles.z,
|
||||||
|
}, // Euler for simplicity
|
||||||
|
scale = new
|
||||||
|
{
|
||||||
|
x = go.transform.localScale.x,
|
||||||
|
y = go.transform.localScale.y,
|
||||||
|
z = go.transform.localScale.z,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ "children", childrenData }
|
{ "children", childrenData },
|
||||||
};
|
};
|
||||||
|
|
||||||
return gameObjectData;
|
return gameObjectData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using UnityMCP.Editor.Helpers;
|
using System.Text.RegularExpressions;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityMcpBridge.Editor.Helpers;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Tools
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles CRUD operations for C# scripts within the Unity project.
|
/// Handles CRUD operations for C# scripts within the Unity project.
|
||||||
|
|
@ -58,7 +58,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
// Basic name validation (alphanumeric, underscores, cannot start with number)
|
// Basic name validation (alphanumeric, underscores, cannot start with number)
|
||||||
if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||||
{
|
{
|
||||||
return Response.Error($"Invalid script name: '{name}'. Use only letters, numbers, underscores, and don't start with a number.");
|
return Response.Error(
|
||||||
|
$"Invalid script name: '{name}'. Use only letters, numbers, underscores, and don't start with a number."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
||||||
|
|
@ -73,7 +75,8 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle empty string case explicitly after processing
|
// Handle empty string case explicitly after processing
|
||||||
if (string.IsNullOrEmpty(relativeDir)) {
|
if (string.IsNullOrEmpty(relativeDir))
|
||||||
|
{
|
||||||
relativeDir = "Scripts"; // Ensure default if path was provided as "" or only "/" or "Assets/"
|
relativeDir = "Scripts"; // Ensure default if path was provided as "" or only "/" or "Assets/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,7 +84,8 @@ namespace UnityMCP.Editor.Tools
|
||||||
string scriptFileName = $"{name}.cs";
|
string scriptFileName = $"{name}.cs";
|
||||||
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Application.dataPath ends in "Assets"
|
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Application.dataPath ends in "Assets"
|
||||||
string fullPath = Path.Combine(fullPathDir, scriptFileName);
|
string fullPath = Path.Combine(fullPathDir, scriptFileName);
|
||||||
string relativePath = Path.Combine("Assets", relativeDir, scriptFileName).Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes
|
string relativePath = Path.Combine("Assets", relativeDir, scriptFileName)
|
||||||
|
.Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes
|
||||||
|
|
||||||
// Ensure the target directory exists for create/update
|
// Ensure the target directory exists for create/update
|
||||||
if (action == "create" || action == "update")
|
if (action == "create" || action == "update")
|
||||||
|
|
@ -92,7 +96,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return Response.Error($"Could not create directory '{fullPathDir}': {e.Message}");
|
return Response.Error(
|
||||||
|
$"Could not create directory '{fullPathDir}': {e.Message}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +106,14 @@ namespace UnityMCP.Editor.Tools
|
||||||
switch (action)
|
switch (action)
|
||||||
{
|
{
|
||||||
case "create":
|
case "create":
|
||||||
return CreateScript(fullPath, relativePath, name, contents, scriptType, namespaceName);
|
return CreateScript(
|
||||||
|
fullPath,
|
||||||
|
relativePath,
|
||||||
|
name,
|
||||||
|
contents,
|
||||||
|
scriptType,
|
||||||
|
namespaceName
|
||||||
|
);
|
||||||
case "read":
|
case "read":
|
||||||
return ReadScript(fullPath, relativePath);
|
return ReadScript(fullPath, relativePath);
|
||||||
case "update":
|
case "update":
|
||||||
|
|
@ -108,7 +121,9 @@ namespace UnityMCP.Editor.Tools
|
||||||
case "delete":
|
case "delete":
|
||||||
return DeleteScript(fullPath, relativePath);
|
return DeleteScript(fullPath, relativePath);
|
||||||
default:
|
default:
|
||||||
return Response.Error($"Unknown action: '{action}'. Valid actions are: create, read, update, delete.");
|
return Response.Error(
|
||||||
|
$"Unknown action: '{action}'. Valid actions are: create, read, update, delete."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,12 +145,21 @@ namespace UnityMCP.Editor.Tools
|
||||||
return Convert.ToBase64String(data);
|
return Convert.ToBase64String(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object CreateScript(string fullPath, string relativePath, string name, string contents, string scriptType, string namespaceName)
|
private static object CreateScript(
|
||||||
|
string fullPath,
|
||||||
|
string relativePath,
|
||||||
|
string name,
|
||||||
|
string contents,
|
||||||
|
string scriptType,
|
||||||
|
string namespaceName
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Check if script already exists
|
// Check if script already exists
|
||||||
if (File.Exists(fullPath))
|
if (File.Exists(fullPath))
|
||||||
{
|
{
|
||||||
return Response.Error($"Script already exists at '{relativePath}'. Use 'update' action to modify.");
|
return Response.Error(
|
||||||
|
$"Script already exists at '{relativePath}'. Use 'update' action to modify."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate default content if none provided
|
// Generate default content if none provided
|
||||||
|
|
@ -157,7 +181,10 @@ namespace UnityMCP.Editor.Tools
|
||||||
File.WriteAllText(fullPath, contents);
|
File.WriteAllText(fullPath, contents);
|
||||||
AssetDatabase.ImportAsset(relativePath);
|
AssetDatabase.ImportAsset(relativePath);
|
||||||
AssetDatabase.Refresh(); // Ensure Unity recognizes the new script
|
AssetDatabase.Refresh(); // Ensure Unity recognizes the new script
|
||||||
return Response.Success($"Script '{name}.cs' created successfully at '{relativePath}'.", new { path = relativePath });
|
return Response.Success(
|
||||||
|
$"Script '{name}.cs' created successfully at '{relativePath}'.",
|
||||||
|
new { path = relativePath }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -178,15 +205,19 @@ namespace UnityMCP.Editor.Tools
|
||||||
|
|
||||||
// Return both normal and encoded contents for larger files
|
// Return both normal and encoded contents for larger files
|
||||||
bool isLarge = contents.Length > 10000; // If content is large, include encoded version
|
bool isLarge = contents.Length > 10000; // If content is large, include encoded version
|
||||||
var responseData = new {
|
var responseData = new
|
||||||
|
{
|
||||||
path = relativePath,
|
path = relativePath,
|
||||||
contents = contents,
|
contents = contents,
|
||||||
// For large files, also include base64-encoded version
|
// For large files, also include base64-encoded version
|
||||||
encodedContents = isLarge ? EncodeBase64(contents) : null,
|
encodedContents = isLarge ? EncodeBase64(contents) : null,
|
||||||
contentsEncoded = isLarge
|
contentsEncoded = isLarge,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.Success($"Script '{Path.GetFileName(relativePath)}' read successfully.", responseData);
|
return Response.Success(
|
||||||
|
$"Script '{Path.GetFileName(relativePath)}' read successfully.",
|
||||||
|
responseData
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -194,11 +225,18 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object UpdateScript(string fullPath, string relativePath, string name, string contents)
|
private static object UpdateScript(
|
||||||
|
string fullPath,
|
||||||
|
string relativePath,
|
||||||
|
string name,
|
||||||
|
string contents
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (!File.Exists(fullPath))
|
if (!File.Exists(fullPath))
|
||||||
{
|
{
|
||||||
return Response.Error($"Script not found at '{relativePath}'. Use 'create' action to add a new script.");
|
return Response.Error(
|
||||||
|
$"Script not found at '{relativePath}'. Use 'create' action to add a new script."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (string.IsNullOrEmpty(contents))
|
if (string.IsNullOrEmpty(contents))
|
||||||
{
|
{
|
||||||
|
|
@ -217,7 +255,10 @@ namespace UnityMCP.Editor.Tools
|
||||||
File.WriteAllText(fullPath, contents);
|
File.WriteAllText(fullPath, contents);
|
||||||
AssetDatabase.ImportAsset(relativePath); // Re-import to reflect changes
|
AssetDatabase.ImportAsset(relativePath); // Re-import to reflect changes
|
||||||
AssetDatabase.Refresh();
|
AssetDatabase.Refresh();
|
||||||
return Response.Success($"Script '{name}.cs' updated successfully at '{relativePath}'.", new { path = relativePath });
|
return Response.Success(
|
||||||
|
$"Script '{name}.cs' updated successfully at '{relativePath}'.",
|
||||||
|
new { path = relativePath }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -239,12 +280,16 @@ namespace UnityMCP.Editor.Tools
|
||||||
if (deleted)
|
if (deleted)
|
||||||
{
|
{
|
||||||
AssetDatabase.Refresh();
|
AssetDatabase.Refresh();
|
||||||
return Response.Success($"Script '{Path.GetFileName(relativePath)}' moved to trash successfully.");
|
return Response.Success(
|
||||||
|
$"Script '{Path.GetFileName(relativePath)}' moved to trash successfully."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fallback or error if MoveAssetToTrash fails
|
// Fallback or error if MoveAssetToTrash fails
|
||||||
return Response.Error($"Failed to move script '{relativePath}' to trash. It might be locked or in use.");
|
return Response.Error(
|
||||||
|
$"Failed to move script '{relativePath}' to trash. It might be locked or in use."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -256,11 +301,16 @@ namespace UnityMCP.Editor.Tools
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates basic C# script content based on name and type.
|
/// Generates basic C# script content based on name and type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string GenerateDefaultScriptContent(string name, string scriptType, string namespaceName)
|
private static string GenerateDefaultScriptContent(
|
||||||
|
string name,
|
||||||
|
string scriptType,
|
||||||
|
string namespaceName
|
||||||
|
)
|
||||||
{
|
{
|
||||||
string usingStatements = "using UnityEngine;\nusing System.Collections;\n";
|
string usingStatements = "using UnityEngine;\nusing System.Collections;\n";
|
||||||
string classDeclaration;
|
string classDeclaration;
|
||||||
string body = "\n // Use this for initialization\n void Start() {\n\n }\n\n // Update is called once per frame\n void Update() {\n\n }\n";
|
string body =
|
||||||
|
"\n // Use this for initialization\n void Start() {\n\n }\n\n // Update is called once per frame\n void Update() {\n\n }\n";
|
||||||
|
|
||||||
string baseClass = "";
|
string baseClass = "";
|
||||||
if (!string.IsNullOrEmpty(scriptType))
|
if (!string.IsNullOrEmpty(scriptType))
|
||||||
|
|
@ -272,7 +322,10 @@ namespace UnityMCP.Editor.Tools
|
||||||
baseClass = " : ScriptableObject";
|
baseClass = " : ScriptableObject";
|
||||||
body = ""; // ScriptableObjects don't usually need Start/Update
|
body = ""; // ScriptableObjects don't usually need Start/Update
|
||||||
}
|
}
|
||||||
else if (scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase) || scriptType.Equals("EditorWindow", StringComparison.OrdinalIgnoreCase))
|
else if (
|
||||||
|
scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| scriptType.Equals("EditorWindow", StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
usingStatements += "using UnityEditor;\n";
|
usingStatements += "using UnityEditor;\n";
|
||||||
if (scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase))
|
if (scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
@ -313,13 +366,16 @@ namespace UnityMCP.Editor.Tools
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool ValidateScriptSyntax(string contents)
|
private static bool ValidateScriptSyntax(string contents)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(contents)) return true; // Empty is technically valid?
|
if (string.IsNullOrEmpty(contents))
|
||||||
|
return true; // Empty is technically valid?
|
||||||
|
|
||||||
int braceBalance = 0;
|
int braceBalance = 0;
|
||||||
foreach (char c in contents)
|
foreach (char c in contents)
|
||||||
{
|
{
|
||||||
if (c == '{') braceBalance++;
|
if (c == '{')
|
||||||
else if (c == '}') braceBalance--;
|
braceBalance++;
|
||||||
|
else if (c == '}')
|
||||||
|
braceBalance--;
|
||||||
}
|
}
|
||||||
|
|
||||||
return braceBalance == 0;
|
return braceBalance == 0;
|
||||||
|
|
@ -328,3 +384,4 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,516 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditorInternal;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityMcpBridge.Editor.Helpers; // For Response class
|
||||||
|
|
||||||
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles reading and clearing Unity Editor console log entries.
|
||||||
|
/// Uses reflection to access internal LogEntry methods/properties.
|
||||||
|
/// </summary>
|
||||||
|
public static class ReadConsole
|
||||||
|
{
|
||||||
|
// Reflection members for accessing internal LogEntry data
|
||||||
|
// private static MethodInfo _getEntriesMethod; // Removed as it's unused and fails reflection
|
||||||
|
private static MethodInfo _startGettingEntriesMethod;
|
||||||
|
private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End...
|
||||||
|
private static MethodInfo _clearMethod;
|
||||||
|
private static MethodInfo _getCountMethod;
|
||||||
|
private static MethodInfo _getEntryMethod;
|
||||||
|
private static FieldInfo _modeField;
|
||||||
|
private static FieldInfo _messageField;
|
||||||
|
private static FieldInfo _fileField;
|
||||||
|
private static FieldInfo _lineField;
|
||||||
|
private static FieldInfo _instanceIdField;
|
||||||
|
|
||||||
|
// Note: Timestamp is not directly available in LogEntry; need to parse message or find alternative?
|
||||||
|
|
||||||
|
// Static constructor for reflection setup
|
||||||
|
static ReadConsole()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Type logEntriesType = typeof(EditorApplication).Assembly.GetType(
|
||||||
|
"UnityEditor.LogEntries"
|
||||||
|
);
|
||||||
|
if (logEntriesType == null)
|
||||||
|
throw new Exception("Could not find internal type UnityEditor.LogEntries");
|
||||||
|
|
||||||
|
// Include NonPublic binding flags as internal APIs might change accessibility
|
||||||
|
BindingFlags staticFlags =
|
||||||
|
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
|
||||||
|
BindingFlags instanceFlags =
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||||
|
|
||||||
|
_startGettingEntriesMethod = logEntriesType.GetMethod(
|
||||||
|
"StartGettingEntries",
|
||||||
|
staticFlags
|
||||||
|
);
|
||||||
|
if (_startGettingEntriesMethod == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntries.StartGettingEntries");
|
||||||
|
|
||||||
|
// Try reflecting EndGettingEntries based on warning message
|
||||||
|
_endGettingEntriesMethod = logEntriesType.GetMethod(
|
||||||
|
"EndGettingEntries",
|
||||||
|
staticFlags
|
||||||
|
);
|
||||||
|
if (_endGettingEntriesMethod == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntries.EndGettingEntries");
|
||||||
|
|
||||||
|
_clearMethod = logEntriesType.GetMethod("Clear", staticFlags);
|
||||||
|
if (_clearMethod == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntries.Clear");
|
||||||
|
|
||||||
|
_getCountMethod = logEntriesType.GetMethod("GetCount", staticFlags);
|
||||||
|
if (_getCountMethod == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntries.GetCount");
|
||||||
|
|
||||||
|
_getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", staticFlags);
|
||||||
|
if (_getEntryMethod == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntries.GetEntryInternal");
|
||||||
|
|
||||||
|
Type logEntryType = typeof(EditorApplication).Assembly.GetType(
|
||||||
|
"UnityEditor.LogEntry"
|
||||||
|
);
|
||||||
|
if (logEntryType == null)
|
||||||
|
throw new Exception("Could not find internal type UnityEditor.LogEntry");
|
||||||
|
|
||||||
|
_modeField = logEntryType.GetField("mode", instanceFlags);
|
||||||
|
if (_modeField == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntry.mode");
|
||||||
|
|
||||||
|
_messageField = logEntryType.GetField("message", instanceFlags);
|
||||||
|
if (_messageField == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntry.message");
|
||||||
|
|
||||||
|
_fileField = logEntryType.GetField("file", instanceFlags);
|
||||||
|
if (_fileField == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntry.file");
|
||||||
|
|
||||||
|
_lineField = logEntryType.GetField("line", instanceFlags);
|
||||||
|
if (_lineField == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntry.line");
|
||||||
|
|
||||||
|
_instanceIdField = logEntryType.GetField("instanceID", instanceFlags);
|
||||||
|
if (_instanceIdField == null)
|
||||||
|
throw new Exception("Failed to reflect LogEntry.instanceID");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogError(
|
||||||
|
$"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries/LogEntry. Console reading/clearing will likely fail. Specific Error: {e.Message}"
|
||||||
|
);
|
||||||
|
// Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this.
|
||||||
|
_startGettingEntriesMethod =
|
||||||
|
_endGettingEntriesMethod =
|
||||||
|
_clearMethod =
|
||||||
|
_getCountMethod =
|
||||||
|
_getEntryMethod =
|
||||||
|
null;
|
||||||
|
_modeField = _messageField = _fileField = _lineField = _instanceIdField = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main Handler ---
|
||||||
|
|
||||||
|
public static object HandleCommand(JObject @params)
|
||||||
|
{
|
||||||
|
// Check if ALL required reflection members were successfully initialized.
|
||||||
|
if (
|
||||||
|
_startGettingEntriesMethod == null
|
||||||
|
|| _endGettingEntriesMethod == null
|
||||||
|
|| _clearMethod == null
|
||||||
|
|| _getCountMethod == null
|
||||||
|
|| _getEntryMethod == null
|
||||||
|
|| _modeField == null
|
||||||
|
|| _messageField == null
|
||||||
|
|| _fileField == null
|
||||||
|
|| _lineField == null
|
||||||
|
|| _instanceIdField == null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Log the error here as well for easier debugging in Unity Console
|
||||||
|
Debug.LogError(
|
||||||
|
"[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue."
|
||||||
|
);
|
||||||
|
return Response.Error(
|
||||||
|
"ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
string action = @params["action"]?.ToString().ToLower() ?? "get";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (action == "clear")
|
||||||
|
{
|
||||||
|
return ClearConsole();
|
||||||
|
}
|
||||||
|
else if (action == "get")
|
||||||
|
{
|
||||||
|
// Extract parameters for 'get'
|
||||||
|
var types =
|
||||||
|
(@params["types"] as JArray)?.Select(t => t.ToString().ToLower()).ToList()
|
||||||
|
?? new List<string> { "error", "warning", "log" };
|
||||||
|
int? count = @params["count"]?.ToObject<int?>();
|
||||||
|
string filterText = @params["filterText"]?.ToString();
|
||||||
|
string sinceTimestampStr = @params["sinceTimestamp"]?.ToString(); // TODO: Implement timestamp filtering
|
||||||
|
string format = (@params["format"]?.ToString() ?? "detailed").ToLower();
|
||||||
|
bool includeStacktrace =
|
||||||
|
@params["includeStacktrace"]?.ToObject<bool?>() ?? true;
|
||||||
|
|
||||||
|
if (types.Contains("all"))
|
||||||
|
{
|
||||||
|
types = new List<string> { "error", "warning", "log" }; // Expand 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(sinceTimestampStr))
|
||||||
|
{
|
||||||
|
Debug.LogWarning(
|
||||||
|
"[ReadConsole] Filtering by 'since_timestamp' is not currently implemented."
|
||||||
|
);
|
||||||
|
// Need a way to get timestamp per log entry.
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetConsoleEntries(types, count, filterText, format, includeStacktrace);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Response.Error(
|
||||||
|
$"Unknown action: '{action}'. Valid actions are 'get' or 'clear'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[ReadConsole] Action '{action}' failed: {e}");
|
||||||
|
return Response.Error($"Internal error processing action '{action}': {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Action Implementations ---
|
||||||
|
|
||||||
|
private static object ClearConsole()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_clearMethod.Invoke(null, null); // Static method, no instance, no parameters
|
||||||
|
return Response.Success("Console cleared successfully.");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[ReadConsole] Failed to clear console: {e}");
|
||||||
|
return Response.Error($"Failed to clear console: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object GetConsoleEntries(
|
||||||
|
List<string> types,
|
||||||
|
int? count,
|
||||||
|
string filterText,
|
||||||
|
string format,
|
||||||
|
bool includeStacktrace
|
||||||
|
)
|
||||||
|
{
|
||||||
|
List<object> formattedEntries = new List<object>();
|
||||||
|
int retrievedCount = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// LogEntries requires calling Start/Stop around GetEntries/GetEntryInternal
|
||||||
|
_startGettingEntriesMethod.Invoke(null, null);
|
||||||
|
|
||||||
|
int totalEntries = (int)_getCountMethod.Invoke(null, null);
|
||||||
|
// Create instance to pass to GetEntryInternal - Ensure the type is correct
|
||||||
|
Type logEntryType = typeof(EditorApplication).Assembly.GetType(
|
||||||
|
"UnityEditor.LogEntry"
|
||||||
|
);
|
||||||
|
if (logEntryType == null)
|
||||||
|
throw new Exception(
|
||||||
|
"Could not find internal type UnityEditor.LogEntry during GetConsoleEntries."
|
||||||
|
);
|
||||||
|
object logEntryInstance = Activator.CreateInstance(logEntryType);
|
||||||
|
|
||||||
|
for (int i = 0; i < totalEntries; i++)
|
||||||
|
{
|
||||||
|
// Get the entry data into our instance using reflection
|
||||||
|
_getEntryMethod.Invoke(null, new object[] { i, logEntryInstance });
|
||||||
|
|
||||||
|
// Extract data using reflection
|
||||||
|
int mode = (int)_modeField.GetValue(logEntryInstance);
|
||||||
|
string message = (string)_messageField.GetValue(logEntryInstance);
|
||||||
|
string file = (string)_fileField.GetValue(logEntryInstance);
|
||||||
|
|
||||||
|
int line = (int)_lineField.GetValue(logEntryInstance);
|
||||||
|
// int instanceId = (int)_instanceIdField.GetValue(logEntryInstance);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(message))
|
||||||
|
continue; // Skip empty messages
|
||||||
|
|
||||||
|
// --- Filtering ---
|
||||||
|
// Filter by type
|
||||||
|
LogType currentType = GetLogTypeFromMode(mode);
|
||||||
|
if (!types.Contains(currentType.ToString().ToLowerInvariant()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by text (case-insensitive)
|
||||||
|
if (
|
||||||
|
!string.IsNullOrEmpty(filterText)
|
||||||
|
&& message.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) < 0
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Filter by timestamp (requires timestamp data)
|
||||||
|
|
||||||
|
// --- Formatting ---
|
||||||
|
string stackTrace = includeStacktrace ? ExtractStackTrace(message) : null;
|
||||||
|
// Get first line if stack is present and requested, otherwise use full message
|
||||||
|
string messageOnly =
|
||||||
|
(includeStacktrace && !string.IsNullOrEmpty(stackTrace))
|
||||||
|
? message.Split(
|
||||||
|
new[] { '\n', '\r' },
|
||||||
|
StringSplitOptions.RemoveEmptyEntries
|
||||||
|
)[0]
|
||||||
|
: message;
|
||||||
|
|
||||||
|
object formattedEntry = null;
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case "plain":
|
||||||
|
formattedEntry = messageOnly;
|
||||||
|
break;
|
||||||
|
case "json":
|
||||||
|
case "detailed": // Treat detailed as json for structured return
|
||||||
|
default:
|
||||||
|
formattedEntry = new
|
||||||
|
{
|
||||||
|
type = currentType.ToString(),
|
||||||
|
message = messageOnly,
|
||||||
|
file = file,
|
||||||
|
line = line,
|
||||||
|
// timestamp = "", // TODO
|
||||||
|
stackTrace = stackTrace, // Will be null if includeStacktrace is false or no stack found
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedEntries.Add(formattedEntry);
|
||||||
|
retrievedCount++;
|
||||||
|
|
||||||
|
// Apply count limit (after filtering)
|
||||||
|
if (count.HasValue && retrievedCount >= count.Value)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}");
|
||||||
|
// Ensure EndGettingEntries is called even if there's an error during iteration
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_endGettingEntriesMethod.Invoke(null, null);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ /* Ignore nested exception */
|
||||||
|
}
|
||||||
|
return Response.Error($"Error retrieving log entries: {e.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Ensure we always call EndGettingEntries
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_endGettingEntriesMethod.Invoke(null, null);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[ReadConsole] Failed to call EndGettingEntries: {e}");
|
||||||
|
// Don't return error here as we might have valid data, but log it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the filtered and formatted list (might be empty)
|
||||||
|
return Response.Success(
|
||||||
|
$"Retrieved {formattedEntries.Count} log entries.",
|
||||||
|
formattedEntries
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal Helpers ---
|
||||||
|
|
||||||
|
// Mapping from LogEntry.mode bits to LogType enum
|
||||||
|
// Based on decompiled UnityEditor code or common patterns. Precise bits might change between Unity versions.
|
||||||
|
// See comments below for LogEntry mode bits exploration.
|
||||||
|
// Note: This mapping is simplified and might not cover all edge cases or future Unity versions perfectly.
|
||||||
|
private const int ModeBitError = 1 << 0;
|
||||||
|
private const int ModeBitAssert = 1 << 1;
|
||||||
|
private const int ModeBitWarning = 1 << 2;
|
||||||
|
private const int ModeBitLog = 1 << 3;
|
||||||
|
private const int ModeBitException = 1 << 4; // Often combined with Error bits
|
||||||
|
private const int ModeBitScriptingError = 1 << 9;
|
||||||
|
private const int ModeBitScriptingWarning = 1 << 10;
|
||||||
|
private const int ModeBitScriptingLog = 1 << 11;
|
||||||
|
private const int ModeBitScriptingException = 1 << 18;
|
||||||
|
private const int ModeBitScriptingAssertion = 1 << 22;
|
||||||
|
|
||||||
|
private static LogType GetLogTypeFromMode(int mode)
|
||||||
|
{
|
||||||
|
// First, determine the type based on the original logic (most severe first)
|
||||||
|
LogType initialType;
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
mode
|
||||||
|
& (
|
||||||
|
ModeBitError
|
||||||
|
| ModeBitScriptingError
|
||||||
|
| ModeBitException
|
||||||
|
| ModeBitScriptingException
|
||||||
|
)
|
||||||
|
) != 0
|
||||||
|
)
|
||||||
|
{
|
||||||
|
initialType = LogType.Error;
|
||||||
|
}
|
||||||
|
else if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0)
|
||||||
|
{
|
||||||
|
initialType = LogType.Assert;
|
||||||
|
}
|
||||||
|
else if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0)
|
||||||
|
{
|
||||||
|
initialType = LogType.Warning;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
initialType = LogType.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the observed "one level lower" correction
|
||||||
|
switch (initialType)
|
||||||
|
{
|
||||||
|
case LogType.Error:
|
||||||
|
return LogType.Warning; // Error becomes Warning
|
||||||
|
case LogType.Warning:
|
||||||
|
return LogType.Log; // Warning becomes Log
|
||||||
|
case LogType.Assert:
|
||||||
|
return LogType.Assert; // Assert remains Assert (no lower level defined)
|
||||||
|
case LogType.Log:
|
||||||
|
return LogType.Log; // Log remains Log
|
||||||
|
default:
|
||||||
|
return LogType.Log; // Default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to extract the stack trace part from a log message.
|
||||||
|
/// Unity log messages often have the stack trace appended after the main message,
|
||||||
|
/// starting on a new line and typically indented or beginning with "at ".
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fullMessage">The complete log message including potential stack trace.</param>
|
||||||
|
/// <returns>The extracted stack trace string, or null if none is found.</returns>
|
||||||
|
private static string ExtractStackTrace(string fullMessage)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(fullMessage))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Split into lines, removing empty ones to handle different line endings gracefully.
|
||||||
|
// Using StringSplitOptions.None might be better if empty lines matter within stack trace, but RemoveEmptyEntries is usually safer here.
|
||||||
|
string[] lines = fullMessage.Split(
|
||||||
|
new[] { '\r', '\n' },
|
||||||
|
StringSplitOptions.RemoveEmptyEntries
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there's only one line or less, there's no separate stack trace.
|
||||||
|
if (lines.Length <= 1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int stackStartIndex = -1;
|
||||||
|
|
||||||
|
// Start checking from the second line onwards.
|
||||||
|
for (int i = 1; i < lines.Length; ++i)
|
||||||
|
{
|
||||||
|
// Performance: TrimStart creates a new string. Consider using IsWhiteSpace check if performance critical.
|
||||||
|
string trimmedLine = lines[i].TrimStart();
|
||||||
|
|
||||||
|
// Check for common stack trace patterns.
|
||||||
|
if (
|
||||||
|
trimmedLine.StartsWith("at ")
|
||||||
|
|| trimmedLine.StartsWith("UnityEngine.")
|
||||||
|
|| trimmedLine.StartsWith("UnityEditor.")
|
||||||
|
|| trimmedLine.Contains("(at ")
|
||||||
|
|| // Covers "(at Assets/..." pattern
|
||||||
|
// Heuristic: Check if line starts with likely namespace/class pattern (Uppercase.Something)
|
||||||
|
(
|
||||||
|
trimmedLine.Length > 0
|
||||||
|
&& char.IsUpper(trimmedLine[0])
|
||||||
|
&& trimmedLine.Contains('.')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
stackStartIndex = i;
|
||||||
|
break; // Found the likely start of the stack trace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a potential start index was found...
|
||||||
|
if (stackStartIndex > 0)
|
||||||
|
{
|
||||||
|
// Join the lines from the stack start index onwards using standard newline characters.
|
||||||
|
// This reconstructs the stack trace part of the message.
|
||||||
|
return string.Join("\n", lines.Skip(stackStartIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No clear stack trace found based on the patterns.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LogEntry.mode bits exploration (based on Unity decompilation/observation):
|
||||||
|
May change between versions.
|
||||||
|
|
||||||
|
Basic Types:
|
||||||
|
kError = 1 << 0 (1)
|
||||||
|
kAssert = 1 << 1 (2)
|
||||||
|
kWarning = 1 << 2 (4)
|
||||||
|
kLog = 1 << 3 (8)
|
||||||
|
kFatal = 1 << 4 (16) - Often treated as Exception/Error
|
||||||
|
|
||||||
|
Modifiers/Context:
|
||||||
|
kAssetImportError = 1 << 7 (128)
|
||||||
|
kAssetImportWarning = 1 << 8 (256)
|
||||||
|
kScriptingError = 1 << 9 (512)
|
||||||
|
kScriptingWarning = 1 << 10 (1024)
|
||||||
|
kScriptingLog = 1 << 11 (2048)
|
||||||
|
kScriptCompileError = 1 << 12 (4096)
|
||||||
|
kScriptCompileWarning = 1 << 13 (8192)
|
||||||
|
kStickyError = 1 << 14 (16384) - Stays visible even after Clear On Play
|
||||||
|
kMayIgnoreLineNumber = 1 << 15 (32768)
|
||||||
|
kReportBug = 1 << 16 (65536) - Shows the "Report Bug" button
|
||||||
|
kDisplayPreviousErrorInStatusBar = 1 << 17 (131072)
|
||||||
|
kScriptingException = 1 << 18 (262144)
|
||||||
|
kDontExtractStacktrace = 1 << 19 (524288) - Hint to the console UI
|
||||||
|
kShouldClearOnPlay = 1 << 20 (1048576) - Default behavior
|
||||||
|
kGraphCompileError = 1 << 21 (2097152)
|
||||||
|
kScriptingAssertion = 1 << 22 (4194304)
|
||||||
|
kVisualScriptingError = 1 << 23 (8388608)
|
||||||
|
|
||||||
|
Example observed values:
|
||||||
|
Log: 2048 (ScriptingLog) or 8 (Log)
|
||||||
|
Warning: 1028 (ScriptingWarning | Warning) or 4 (Warning)
|
||||||
|
Error: 513 (ScriptingError | Error) or 1 (Error)
|
||||||
|
Exception: 262161 (ScriptingException | Error | kFatal?) - Complex combination
|
||||||
|
Assertion: 4194306 (ScriptingAssertion | Assert) or 2 (Assert)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,26 +1,29 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using System.IO;
|
using UnityEditor;
|
||||||
using UnityMCP.Editor.Models;
|
using UnityEngine;
|
||||||
using UnityMCP.Editor.Tools;
|
using UnityMcpBridge.Editor.Models;
|
||||||
|
using UnityMcpBridge.Editor.Tools;
|
||||||
|
|
||||||
namespace UnityMCP.Editor
|
namespace UnityMcpBridge.Editor
|
||||||
{
|
{
|
||||||
[InitializeOnLoad]
|
[InitializeOnLoad]
|
||||||
public static partial class UnityMCPBridge
|
public static partial class UnityMcpBridge
|
||||||
{
|
{
|
||||||
private static TcpListener listener;
|
private static TcpListener listener;
|
||||||
private static bool isRunning = false;
|
private static bool isRunning = false;
|
||||||
private static readonly object lockObj = new();
|
private static readonly object lockObj = new();
|
||||||
private static Dictionary<string, (string commandJson, TaskCompletionSource<string> tcs)> commandQueue = new();
|
private static Dictionary<
|
||||||
|
string,
|
||||||
|
(string commandJson, TaskCompletionSource<string> tcs)
|
||||||
|
> commandQueue = new();
|
||||||
private static readonly int unityPort = 6400; // Hardcoded port
|
private static readonly int unityPort = 6400; // Hardcoded port
|
||||||
|
|
||||||
public static bool IsRunning => isRunning;
|
public static bool IsRunning => isRunning;
|
||||||
|
|
@ -33,11 +36,14 @@ namespace UnityMCP.Editor
|
||||||
if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase))
|
if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
string fullPath = Path.Combine(Application.dataPath, path.StartsWith("Assets/") ? path.Substring(7) : path);
|
string fullPath = Path.Combine(
|
||||||
|
Application.dataPath,
|
||||||
|
path.StartsWith("Assets/") ? path.Substring(7) : path
|
||||||
|
);
|
||||||
return Directory.Exists(fullPath);
|
return Directory.Exists(fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
static UnityMCPBridge()
|
static UnityMcpBridge()
|
||||||
{
|
{
|
||||||
Start();
|
Start();
|
||||||
EditorApplication.quitting += Stop;
|
EditorApplication.quitting += Stop;
|
||||||
|
|
@ -45,7 +51,8 @@ namespace UnityMCP.Editor
|
||||||
|
|
||||||
public static void Start()
|
public static void Start()
|
||||||
{
|
{
|
||||||
if (isRunning) return;
|
if (isRunning)
|
||||||
|
return;
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
listener = new TcpListener(IPAddress.Loopback, unityPort);
|
listener = new TcpListener(IPAddress.Loopback, unityPort);
|
||||||
listener.Start();
|
listener.Start();
|
||||||
|
|
@ -56,7 +63,8 @@ namespace UnityMCP.Editor
|
||||||
|
|
||||||
public static void Stop()
|
public static void Stop()
|
||||||
{
|
{
|
||||||
if (!isRunning) return;
|
if (!isRunning)
|
||||||
|
return;
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
listener.Stop();
|
listener.Stop();
|
||||||
EditorApplication.update -= ProcessCommands;
|
EditorApplication.update -= ProcessCommands;
|
||||||
|
|
@ -71,7 +79,11 @@ namespace UnityMCP.Editor
|
||||||
{
|
{
|
||||||
var client = await listener.AcceptTcpClientAsync();
|
var client = await listener.AcceptTcpClientAsync();
|
||||||
// Enable basic socket keepalive
|
// Enable basic socket keepalive
|
||||||
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
client.Client.SetSocketOption(
|
||||||
|
SocketOptionLevel.Socket,
|
||||||
|
SocketOptionName.KeepAlive,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Set longer receive timeout to prevent quick disconnections
|
// Set longer receive timeout to prevent quick disconnections
|
||||||
client.ReceiveTimeout = 60000; // 60 seconds
|
client.ReceiveTimeout = 60000; // 60 seconds
|
||||||
|
|
@ -81,7 +93,8 @@ namespace UnityMCP.Editor
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (isRunning) Debug.LogError($"Listener error: {ex.Message}");
|
if (isRunning)
|
||||||
|
Debug.LogError($"Listener error: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,9 +110,14 @@ namespace UnityMCP.Editor
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
|
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
|
||||||
if (bytesRead == 0) break; // Client disconnected
|
if (bytesRead == 0)
|
||||||
|
break; // Client disconnected
|
||||||
|
|
||||||
string commandText = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
string commandText = System.Text.Encoding.UTF8.GetString(
|
||||||
|
buffer,
|
||||||
|
0,
|
||||||
|
bytesRead
|
||||||
|
);
|
||||||
string commandId = Guid.NewGuid().ToString();
|
string commandId = Guid.NewGuid().ToString();
|
||||||
var tcs = new TaskCompletionSource<string>();
|
var tcs = new TaskCompletionSource<string>();
|
||||||
|
|
||||||
|
|
@ -107,7 +125,9 @@ namespace UnityMCP.Editor
|
||||||
if (commandText.Trim() == "ping")
|
if (commandText.Trim() == "ping")
|
||||||
{
|
{
|
||||||
// Direct response to ping without going through JSON parsing
|
// Direct response to ping without going through JSON parsing
|
||||||
byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes("{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}");
|
byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes(
|
||||||
|
"{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}"
|
||||||
|
);
|
||||||
await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length);
|
await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +169,7 @@ namespace UnityMCP.Editor
|
||||||
var emptyResponse = new
|
var emptyResponse = new
|
||||||
{
|
{
|
||||||
status = "error",
|
status = "error",
|
||||||
error = "Empty command received"
|
error = "Empty command received",
|
||||||
};
|
};
|
||||||
tcs.SetResult(JsonConvert.SerializeObject(emptyResponse));
|
tcs.SetResult(JsonConvert.SerializeObject(emptyResponse));
|
||||||
processedIds.Add(id);
|
processedIds.Add(id);
|
||||||
|
|
@ -165,7 +185,7 @@ namespace UnityMCP.Editor
|
||||||
var pingResponse = new
|
var pingResponse = new
|
||||||
{
|
{
|
||||||
status = "success",
|
status = "success",
|
||||||
result = new { message = "pong" }
|
result = new { message = "pong" },
|
||||||
};
|
};
|
||||||
tcs.SetResult(JsonConvert.SerializeObject(pingResponse));
|
tcs.SetResult(JsonConvert.SerializeObject(pingResponse));
|
||||||
processedIds.Add(id);
|
processedIds.Add(id);
|
||||||
|
|
@ -179,7 +199,9 @@ namespace UnityMCP.Editor
|
||||||
{
|
{
|
||||||
status = "error",
|
status = "error",
|
||||||
error = "Invalid JSON format",
|
error = "Invalid JSON format",
|
||||||
receivedText = commandText.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText
|
receivedText = commandText.Length > 50
|
||||||
|
? commandText.Substring(0, 50) + "..."
|
||||||
|
: commandText,
|
||||||
};
|
};
|
||||||
tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse));
|
tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse));
|
||||||
processedIds.Add(id);
|
processedIds.Add(id);
|
||||||
|
|
@ -194,7 +216,7 @@ namespace UnityMCP.Editor
|
||||||
{
|
{
|
||||||
status = "error",
|
status = "error",
|
||||||
error = "Command deserialized to null",
|
error = "Command deserialized to null",
|
||||||
details = "The command was valid JSON but could not be deserialized to a Command object"
|
details = "The command was valid JSON but could not be deserialized to a Command object",
|
||||||
};
|
};
|
||||||
tcs.SetResult(JsonConvert.SerializeObject(nullCommandResponse));
|
tcs.SetResult(JsonConvert.SerializeObject(nullCommandResponse));
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +235,9 @@ namespace UnityMCP.Editor
|
||||||
status = "error",
|
status = "error",
|
||||||
error = ex.Message,
|
error = ex.Message,
|
||||||
commandType = "Unknown (error during processing)",
|
commandType = "Unknown (error during processing)",
|
||||||
receivedText = commandText?.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText
|
receivedText = commandText?.Length > 50
|
||||||
|
? commandText.Substring(0, 50) + "..."
|
||||||
|
: commandText,
|
||||||
};
|
};
|
||||||
string responseJson = JsonConvert.SerializeObject(response);
|
string responseJson = JsonConvert.SerializeObject(response);
|
||||||
tcs.SetResult(responseJson);
|
tcs.SetResult(responseJson);
|
||||||
|
|
@ -236,8 +260,11 @@ namespace UnityMCP.Editor
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
text = text.Trim();
|
text = text.Trim();
|
||||||
if ((text.StartsWith("{") && text.EndsWith("}")) || // Object
|
if (
|
||||||
(text.StartsWith("[") && text.EndsWith("]"))) // Array
|
(text.StartsWith("{") && text.EndsWith("}"))
|
||||||
|
|| // Object
|
||||||
|
(text.StartsWith("[") && text.EndsWith("]"))
|
||||||
|
) // Array
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -263,7 +290,7 @@ namespace UnityMCP.Editor
|
||||||
{
|
{
|
||||||
status = "error",
|
status = "error",
|
||||||
error = "Command type cannot be empty",
|
error = "Command type cannot be empty",
|
||||||
details = "A valid command type is required for processing"
|
details = "A valid command type is required for processing",
|
||||||
};
|
};
|
||||||
return JsonConvert.SerializeObject(errorResponse);
|
return JsonConvert.SerializeObject(errorResponse);
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +298,11 @@ namespace UnityMCP.Editor
|
||||||
// Handle ping command for connection verification
|
// Handle ping command for connection verification
|
||||||
if (command.type.Equals("ping", StringComparison.OrdinalIgnoreCase))
|
if (command.type.Equals("ping", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var pingResponse = new { status = "success", result = new { message = "pong" } };
|
var pingResponse = new
|
||||||
|
{
|
||||||
|
status = "success",
|
||||||
|
result = new { message = "pong" },
|
||||||
|
};
|
||||||
return JsonConvert.SerializeObject(pingResponse);
|
return JsonConvert.SerializeObject(pingResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,7 +321,9 @@ namespace UnityMCP.Editor
|
||||||
"manage_asset" => ManageAsset.HandleCommand(paramsObject),
|
"manage_asset" => ManageAsset.HandleCommand(paramsObject),
|
||||||
"read_console" => ReadConsole.HandleCommand(paramsObject),
|
"read_console" => ReadConsole.HandleCommand(paramsObject),
|
||||||
"execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject),
|
"execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject),
|
||||||
_ => throw new ArgumentException($"Unknown or unsupported command type: {command.type}")
|
_ => throw new ArgumentException(
|
||||||
|
$"Unknown or unsupported command type: {command.type}"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Standard success response format
|
// Standard success response format
|
||||||
|
|
@ -300,7 +333,9 @@ namespace UnityMCP.Editor
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Log the detailed error in Unity for debugging
|
// Log the detailed error in Unity for debugging
|
||||||
Debug.LogError($"Error executing command '{command?.type ?? "Unknown"}': {ex.Message}\n{ex.StackTrace}");
|
Debug.LogError(
|
||||||
|
$"Error executing command '{command?.type ?? "Unknown"}': {ex.Message}\n{ex.StackTrace}"
|
||||||
|
);
|
||||||
|
|
||||||
// Standard error response format
|
// Standard error response format
|
||||||
var response = new
|
var response = new
|
||||||
|
|
@ -309,7 +344,9 @@ namespace UnityMCP.Editor
|
||||||
error = ex.Message, // Provide the specific error message
|
error = ex.Message, // Provide the specific error message
|
||||||
command = command?.type ?? "Unknown", // Include the command type if available
|
command = command?.type ?? "Unknown", // Include the command type if available
|
||||||
stackTrace = ex.StackTrace, // Include stack trace for detailed debugging
|
stackTrace = ex.StackTrace, // Include stack trace for detailed debugging
|
||||||
paramsSummary = command?.@params != null ? GetParamsSummary(command.@params) : "No parameters" // Summarize parameters for context
|
paramsSummary = command?.@params != null
|
||||||
|
? GetParamsSummary(command.@params)
|
||||||
|
: "No parameters", // Summarize parameters for context
|
||||||
};
|
};
|
||||||
return JsonConvert.SerializeObject(response);
|
return JsonConvert.SerializeObject(response);
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +360,14 @@ namespace UnityMCP.Editor
|
||||||
if (@params == null || !@params.HasValues)
|
if (@params == null || !@params.HasValues)
|
||||||
return "No parameters";
|
return "No parameters";
|
||||||
|
|
||||||
return string.Join(", ", @params.Properties().Select(p => $"{p.Name}: {p.Value?.ToString()?.Substring(0, Math.Min(20, p.Value?.ToString()?.Length ?? 0))}"));
|
return string.Join(
|
||||||
|
", ",
|
||||||
|
@params
|
||||||
|
.Properties()
|
||||||
|
.Select(p =>
|
||||||
|
$"{p.Name}: {p.Value?.ToString()?.Substring(0, Math.Min(20, p.Value?.ToString()?.Length ?? 0))}"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using UnityMCP.Editor.Models;
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityMcpBridge.Editor.Models;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Windows
|
namespace UnityMcpBridge.Editor.Windows
|
||||||
{
|
{
|
||||||
// Editor window to display manual configuration instructions
|
// Editor window to display manual configuration instructions
|
||||||
public class ManualConfigEditorWindow : EditorWindow
|
public class ManualConfigEditorWindow : EditorWindow
|
||||||
|
|
@ -33,36 +33,65 @@ namespace UnityMCP.Editor.Windows
|
||||||
// Header with improved styling
|
// Header with improved styling
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
|
Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
|
||||||
EditorGUI.DrawRect(new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), new Color(0.2f, 0.2f, 0.2f, 0.1f));
|
EditorGUI.DrawRect(
|
||||||
GUI.Label(new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
|
new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
|
||||||
mcpClient.name + " Manual Configuration", EditorStyles.boldLabel);
|
new Color(0.2f, 0.2f, 0.2f, 0.1f)
|
||||||
|
);
|
||||||
|
GUI.Label(
|
||||||
|
new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
|
||||||
|
mcpClient.name + " Manual Configuration",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
// Instructions with improved styling
|
// Instructions with improved styling
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
||||||
Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
|
Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
|
||||||
EditorGUI.DrawRect(new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height), new Color(0.1f, 0.1f, 0.1f, 0.2f));
|
EditorGUI.DrawRect(
|
||||||
GUI.Label(new Rect(headerRect.x + 8, headerRect.y + 4, headerRect.width - 16, headerRect.height),
|
new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
|
||||||
"The automatic configuration failed. Please follow these steps:", EditorStyles.boldLabel);
|
new Color(0.1f, 0.1f, 0.1f, 0.2f)
|
||||||
|
);
|
||||||
|
GUI.Label(
|
||||||
|
new Rect(
|
||||||
|
headerRect.x + 8,
|
||||||
|
headerRect.y + 4,
|
||||||
|
headerRect.width - 16,
|
||||||
|
headerRect.height
|
||||||
|
),
|
||||||
|
"The automatic configuration failed. Please follow these steps:",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
|
GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
|
||||||
{
|
{
|
||||||
margin = new RectOffset(10, 10, 5, 5)
|
margin = new RectOffset(10, 10, 5, 5),
|
||||||
};
|
};
|
||||||
|
|
||||||
EditorGUILayout.LabelField("1. Open " + mcpClient.name + " config file by either:", instructionStyle);
|
EditorGUILayout.LabelField(
|
||||||
|
"1. Open " + mcpClient.name + " config file by either:",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
if (mcpClient.mcpType == McpTypes.ClaudeDesktop)
|
if (mcpClient.mcpType == McpTypes.ClaudeDesktop)
|
||||||
{
|
{
|
||||||
EditorGUILayout.LabelField(" a) Going to Settings > Developer > Edit Config", instructionStyle);
|
EditorGUILayout.LabelField(
|
||||||
|
" a) Going to Settings > Developer > Edit Config",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else if (mcpClient.mcpType == McpTypes.Cursor)
|
else if (mcpClient.mcpType == McpTypes.Cursor)
|
||||||
{
|
{
|
||||||
EditorGUILayout.LabelField(" a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server", instructionStyle);
|
EditorGUILayout.LabelField(
|
||||||
|
" a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
}
|
}
|
||||||
EditorGUILayout.LabelField(" OR", instructionStyle);
|
EditorGUILayout.LabelField(" OR", instructionStyle);
|
||||||
EditorGUILayout.LabelField(" b) Opening the configuration file at:", instructionStyle);
|
EditorGUILayout.LabelField(
|
||||||
|
" b) Opening the configuration file at:",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
|
||||||
// Path section with improved styling
|
// Path section with improved styling
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
@ -71,7 +100,10 @@ namespace UnityMCP.Editor.Windows
|
||||||
{
|
{
|
||||||
displayPath = mcpClient.windowsConfigPath;
|
displayPath = mcpClient.windowsConfigPath;
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
else if (
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
|
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
displayPath = mcpClient.linuxConfigPath;
|
displayPath = mcpClient.linuxConfigPath;
|
||||||
}
|
}
|
||||||
|
|
@ -81,12 +113,13 @@ namespace UnityMCP.Editor.Windows
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent text overflow by allowing the text field to wrap
|
// Prevent text overflow by allowing the text field to wrap
|
||||||
GUIStyle pathStyle = new(EditorStyles.textField)
|
GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };
|
||||||
{
|
|
||||||
wordWrap = true
|
|
||||||
};
|
|
||||||
|
|
||||||
EditorGUILayout.TextField(displayPath, pathStyle, GUILayout.Height(EditorGUIUtility.singleLineHeight));
|
EditorGUILayout.TextField(
|
||||||
|
displayPath,
|
||||||
|
pathStyle,
|
||||||
|
GUILayout.Height(EditorGUIUtility.singleLineHeight)
|
||||||
|
);
|
||||||
|
|
||||||
// Copy button with improved styling
|
// Copy button with improved styling
|
||||||
EditorGUILayout.BeginHorizontal();
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
|
@ -94,24 +127,40 @@ namespace UnityMCP.Editor.Windows
|
||||||
GUIStyle copyButtonStyle = new(GUI.skin.button)
|
GUIStyle copyButtonStyle = new(GUI.skin.button)
|
||||||
{
|
{
|
||||||
padding = new RectOffset(15, 15, 5, 5),
|
padding = new RectOffset(15, 15, 5, 5),
|
||||||
margin = new RectOffset(10, 10, 5, 5)
|
margin = new RectOffset(10, 10, 5, 5),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (GUILayout.Button("Copy Path", copyButtonStyle, GUILayout.Height(25), GUILayout.Width(100)))
|
if (
|
||||||
|
GUILayout.Button(
|
||||||
|
"Copy Path",
|
||||||
|
copyButtonStyle,
|
||||||
|
GUILayout.Height(25),
|
||||||
|
GUILayout.Width(100)
|
||||||
|
)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
EditorGUIUtility.systemCopyBuffer = displayPath;
|
EditorGUIUtility.systemCopyBuffer = displayPath;
|
||||||
pathCopied = true;
|
pathCopied = true;
|
||||||
copyFeedbackTimer = 2f;
|
copyFeedbackTimer = 2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GUILayout.Button("Open File", copyButtonStyle, GUILayout.Height(25), GUILayout.Width(100)))
|
if (
|
||||||
|
GUILayout.Button(
|
||||||
|
"Open File",
|
||||||
|
copyButtonStyle,
|
||||||
|
GUILayout.Height(25),
|
||||||
|
GUILayout.Width(100)
|
||||||
|
)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Open the file using the system's default application
|
// Open the file using the system's default application
|
||||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
System.Diagnostics.Process.Start(
|
||||||
|
new System.Diagnostics.ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = displayPath,
|
FileName = displayPath,
|
||||||
UseShellExecute = true
|
UseShellExecute = true,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathCopied)
|
if (pathCopied)
|
||||||
|
|
@ -126,7 +175,10 @@ namespace UnityMCP.Editor.Windows
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
EditorGUILayout.LabelField("2. Paste the following JSON configuration:", instructionStyle);
|
EditorGUILayout.LabelField(
|
||||||
|
"2. Paste the following JSON configuration:",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
|
||||||
// JSON section with improved styling
|
// JSON section with improved styling
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
@ -135,7 +187,7 @@ namespace UnityMCP.Editor.Windows
|
||||||
GUIStyle jsonStyle = new(EditorStyles.textArea)
|
GUIStyle jsonStyle = new(EditorStyles.textArea)
|
||||||
{
|
{
|
||||||
font = EditorStyles.boldFont,
|
font = EditorStyles.boldFont,
|
||||||
wordWrap = true
|
wordWrap = true,
|
||||||
};
|
};
|
||||||
jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue
|
jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue
|
||||||
|
|
||||||
|
|
@ -146,7 +198,14 @@ namespace UnityMCP.Editor.Windows
|
||||||
EditorGUILayout.BeginHorizontal();
|
EditorGUILayout.BeginHorizontal();
|
||||||
GUILayout.FlexibleSpace();
|
GUILayout.FlexibleSpace();
|
||||||
|
|
||||||
if (GUILayout.Button("Copy JSON", copyButtonStyle, GUILayout.Height(25), GUILayout.Width(100)))
|
if (
|
||||||
|
GUILayout.Button(
|
||||||
|
"Copy JSON",
|
||||||
|
copyButtonStyle,
|
||||||
|
GUILayout.Height(25),
|
||||||
|
GUILayout.Width(100)
|
||||||
|
)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
EditorGUIUtility.systemCopyBuffer = configJson;
|
EditorGUIUtility.systemCopyBuffer = configJson;
|
||||||
jsonCopied = true;
|
jsonCopied = true;
|
||||||
|
|
@ -164,7 +223,10 @@ namespace UnityMCP.Editor.Windows
|
||||||
EditorGUILayout.EndVertical();
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
EditorGUILayout.LabelField("3. Save the file and restart " + mcpClient.name, instructionStyle);
|
EditorGUILayout.LabelField(
|
||||||
|
"3. Save the file and restart " + mcpClient.name,
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
|
||||||
EditorGUILayout.EndVertical();
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System;
|
using System;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Text;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.IO;
|
||||||
using UnityMCP.Editor.Models;
|
using System.Net.Sockets;
|
||||||
using UnityMCP.Editor.Data;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityMcpBridge.Editor.Data;
|
||||||
|
using UnityMcpBridge.Editor.Models;
|
||||||
|
|
||||||
namespace UnityMCP.Editor.Windows
|
namespace UnityMcpBridge.Editor.Windows
|
||||||
{
|
{
|
||||||
public class UnityMCPEditorWindow : EditorWindow
|
public class UnityMcpEditorWindow : EditorWindow
|
||||||
{
|
{
|
||||||
private bool isUnityBridgeRunning = false;
|
private bool isUnityBridgeRunning = false;
|
||||||
private Vector2 scrollPosition;
|
private Vector2 scrollPosition;
|
||||||
|
|
@ -31,22 +29,51 @@ namespace UnityMCP.Editor.Windows
|
||||||
|
|
||||||
private List<string> possiblePaths = new()
|
private List<string> possiblePaths = new()
|
||||||
{
|
{
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python", "server.py")),
|
Path.GetFullPath(
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py")),
|
Path.Combine(Application.dataPath, "unity-mcp", "Python", "server.py")
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Library", "PackageCache", "com.justinpbarnett.unity-mcp@*", "Python", "server.py")),
|
),
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py"))
|
Path.GetFullPath(
|
||||||
|
Path.Combine(
|
||||||
|
Application.dataPath,
|
||||||
|
"Packages",
|
||||||
|
"com.justinpbarnett.unity-mcp",
|
||||||
|
"Python",
|
||||||
|
"server.py"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Path.GetFullPath(
|
||||||
|
Path.Combine(
|
||||||
|
Application.dataPath,
|
||||||
|
"..",
|
||||||
|
"Library",
|
||||||
|
"PackageCache",
|
||||||
|
"com.justinpbarnett.unity-mcp@*",
|
||||||
|
"Python",
|
||||||
|
"server.py"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Path.GetFullPath(
|
||||||
|
Path.Combine(
|
||||||
|
Application.dataPath,
|
||||||
|
"..",
|
||||||
|
"Packages",
|
||||||
|
"com.justinpbarnett.unity-mcp",
|
||||||
|
"Python",
|
||||||
|
"server.py"
|
||||||
|
)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
[MenuItem("Window/Unity MCP")]
|
[MenuItem("Window/Unity MCP")]
|
||||||
public static void ShowWindow()
|
public static void ShowWindow()
|
||||||
{
|
{
|
||||||
GetWindow<UnityMCPEditorWindow>("MCP Editor");
|
GetWindow<UnityMcpEditorWindow>("MCP Editor");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
// Check initial states
|
// Check initial states
|
||||||
isUnityBridgeRunning = UnityMCPBridge.IsRunning;
|
isUnityBridgeRunning = UnityMcpBridge.IsRunning;
|
||||||
CheckPythonServerConnection();
|
CheckPythonServerConnection();
|
||||||
foreach (McpClient mcpClient in mcpClients.clients)
|
foreach (McpClient mcpClient in mcpClients.clients)
|
||||||
{
|
{
|
||||||
|
|
@ -107,7 +134,9 @@ namespace UnityMCP.Editor.Windows
|
||||||
{
|
{
|
||||||
// Received response but not the expected one
|
// Received response but not the expected one
|
||||||
pythonServerStatus = "Invalid Server";
|
pythonServerStatus = "Invalid Server";
|
||||||
pythonServerStatusColor = GetStatusColor(McpStatus.CommunicationError);
|
pythonServerStatusColor = GetStatusColor(
|
||||||
|
McpStatus.CommunicationError
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -115,7 +144,9 @@ namespace UnityMCP.Editor.Windows
|
||||||
// No response received
|
// No response received
|
||||||
pythonServerStatus = "No Response";
|
pythonServerStatus = "No Response";
|
||||||
pythonServerStatusColor = GetStatusColor(McpStatus.NoResponse);
|
pythonServerStatusColor = GetStatusColor(McpStatus.NoResponse);
|
||||||
UnityEngine.Debug.LogWarning($"Python server not responding on port {unityPort}");
|
UnityEngine.Debug.LogWarning(
|
||||||
|
$"Python server not responding on port {unityPort}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -123,7 +154,9 @@ namespace UnityMCP.Editor.Windows
|
||||||
// Connection established but communication failed
|
// Connection established but communication failed
|
||||||
pythonServerStatus = "Communication Error";
|
pythonServerStatus = "Communication Error";
|
||||||
pythonServerStatusColor = GetStatusColor(McpStatus.CommunicationError);
|
pythonServerStatusColor = GetStatusColor(McpStatus.CommunicationError);
|
||||||
UnityEngine.Debug.LogWarning($"Error communicating with Python server: {e.Message}");
|
UnityEngine.Debug.LogWarning(
|
||||||
|
$"Error communicating with Python server: {e.Message}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -131,7 +164,9 @@ namespace UnityMCP.Editor.Windows
|
||||||
// Connection failed
|
// Connection failed
|
||||||
pythonServerStatus = "Not Connected";
|
pythonServerStatus = "Not Connected";
|
||||||
pythonServerStatusColor = GetStatusColor(McpStatus.NotConfigured);
|
pythonServerStatusColor = GetStatusColor(McpStatus.NotConfigured);
|
||||||
UnityEngine.Debug.LogWarning($"Python server is not running or not accessible on port {unityPort}");
|
UnityEngine.Debug.LogWarning(
|
||||||
|
$"Python server is not running or not accessible on port {unityPort}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
client.Close();
|
client.Close();
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +190,7 @@ namespace UnityMCP.Editor.Windows
|
||||||
McpStatus.IncorrectPath => Color.yellow,
|
McpStatus.IncorrectPath => Color.yellow,
|
||||||
McpStatus.CommunicationError => Color.yellow,
|
McpStatus.CommunicationError => Color.yellow,
|
||||||
McpStatus.NoResponse => Color.yellow,
|
McpStatus.NoResponse => Color.yellow,
|
||||||
_ => Color.red // Default to red for error states or not configured
|
_ => Color.red, // Default to red for error states or not configured
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,8 +213,16 @@ namespace UnityMCP.Editor.Windows
|
||||||
// Header with improved styling
|
// Header with improved styling
|
||||||
EditorGUILayout.Space(5);
|
EditorGUILayout.Space(5);
|
||||||
Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
|
Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
|
||||||
GUI.Label(new Rect(headerRect.x + 8, headerRect.y + 4, headerRect.width - 16, headerRect.height),
|
GUI.Label(
|
||||||
mcpClient.name + " Configuration", EditorStyles.boldLabel);
|
new Rect(
|
||||||
|
headerRect.x + 8,
|
||||||
|
headerRect.y + 4,
|
||||||
|
headerRect.width - 16,
|
||||||
|
headerRect.height
|
||||||
|
),
|
||||||
|
mcpClient.name + " Configuration",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
EditorGUILayout.Space(5);
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
// Status indicator with colored dot
|
// Status indicator with colored dot
|
||||||
|
|
@ -190,7 +233,11 @@ namespace UnityMCP.Editor.Windows
|
||||||
DrawStatusDot(statusRect, statusColor);
|
DrawStatusDot(statusRect, statusColor);
|
||||||
|
|
||||||
// Status text with some padding
|
// Status text with some padding
|
||||||
EditorGUILayout.LabelField(new GUIContent(" " + mcpClient.configStatus), GUILayout.Height(20), GUILayout.MinWidth(100));
|
EditorGUILayout.LabelField(
|
||||||
|
new GUIContent(" " + mcpClient.configStatus),
|
||||||
|
GUILayout.Height(20),
|
||||||
|
GUILayout.MinWidth(100)
|
||||||
|
);
|
||||||
EditorGUILayout.EndHorizontal();
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
EditorGUILayout.Space(8);
|
EditorGUILayout.Space(8);
|
||||||
|
|
@ -203,7 +250,13 @@ namespace UnityMCP.Editor.Windows
|
||||||
// Create muted button style for Manual Setup
|
// Create muted button style for Manual Setup
|
||||||
GUIStyle mutedButtonStyle = new(buttonStyle);
|
GUIStyle mutedButtonStyle = new(buttonStyle);
|
||||||
|
|
||||||
if (GUILayout.Button($"Auto Configure {mcpClient.name}", buttonStyle, GUILayout.Height(28)))
|
if (
|
||||||
|
GUILayout.Button(
|
||||||
|
$"Auto Configure {mcpClient.name}",
|
||||||
|
buttonStyle,
|
||||||
|
GUILayout.Height(28)
|
||||||
|
)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
ConfigureMcpClient(mcpClient);
|
ConfigureMcpClient(mcpClient);
|
||||||
}
|
}
|
||||||
|
|
@ -227,7 +280,10 @@ namespace UnityMCP.Editor.Windows
|
||||||
EditorGUILayout.Space(5);
|
EditorGUILayout.Space(5);
|
||||||
}
|
}
|
||||||
// Add space and end the horizontal layout if last item is odd
|
// Add space and end the horizontal layout if last item is odd
|
||||||
else if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) == mcpClients.clients.Count - 1)
|
else if (
|
||||||
|
useHalfWidth
|
||||||
|
&& mcpClients.clients.IndexOf(mcpClient) == mcpClients.clients.Count - 1
|
||||||
|
)
|
||||||
{
|
{
|
||||||
EditorGUILayout.EndHorizontal();
|
EditorGUILayout.EndHorizontal();
|
||||||
EditorGUILayout.Space(5);
|
EditorGUILayout.Space(5);
|
||||||
|
|
@ -245,7 +301,11 @@ namespace UnityMCP.Editor.Windows
|
||||||
Handles.DrawSolidDisc(center, Vector3.forward, radius);
|
Handles.DrawSolidDisc(center, Vector3.forward, radius);
|
||||||
|
|
||||||
// Draw the border
|
// Draw the border
|
||||||
Color borderColor = new(statusColor.r * 0.7f, statusColor.g * 0.7f, statusColor.b * 0.7f);
|
Color borderColor = new(
|
||||||
|
statusColor.r * 0.7f,
|
||||||
|
statusColor.g * 0.7f,
|
||||||
|
statusColor.b * 0.7f
|
||||||
|
);
|
||||||
Handles.color = borderColor;
|
Handles.color = borderColor;
|
||||||
Handles.DrawWireDisc(center, Vector3.forward, radius);
|
Handles.DrawWireDisc(center, Vector3.forward, radius);
|
||||||
}
|
}
|
||||||
|
|
@ -257,9 +317,15 @@ namespace UnityMCP.Editor.Windows
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
// Title with improved styling
|
// Title with improved styling
|
||||||
Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
|
Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
|
||||||
EditorGUI.DrawRect(new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), new Color(0.2f, 0.2f, 0.2f, 0.1f));
|
EditorGUI.DrawRect(
|
||||||
GUI.Label(new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
|
new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
|
||||||
"MCP Editor", EditorStyles.boldLabel);
|
new Color(0.2f, 0.2f, 0.2f, 0.1f)
|
||||||
|
);
|
||||||
|
GUI.Label(
|
||||||
|
new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
|
||||||
|
"MCP Editor",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
// Python Server Status Section
|
// Python Server Status Section
|
||||||
|
|
@ -274,7 +340,10 @@ namespace UnityMCP.Editor.Windows
|
||||||
|
|
||||||
EditorGUILayout.LabelField($"Unity Port: {unityPort}");
|
EditorGUILayout.LabelField($"Unity Port: {unityPort}");
|
||||||
EditorGUILayout.LabelField($"MCP Port: {mcpPort}");
|
EditorGUILayout.LabelField($"MCP Port: {mcpPort}");
|
||||||
EditorGUILayout.HelpBox("Your MCP client (e.g. Cursor or Claude Desktop) will start the server automatically when you start it.", MessageType.Info);
|
EditorGUILayout.HelpBox(
|
||||||
|
"Your MCP client (e.g. Cursor or Claude Desktop) will start the server automatically when you start it.",
|
||||||
|
MessageType.Info
|
||||||
|
);
|
||||||
EditorGUILayout.EndVertical();
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
|
|
@ -304,23 +373,23 @@ namespace UnityMCP.Editor.Windows
|
||||||
{
|
{
|
||||||
if (isUnityBridgeRunning)
|
if (isUnityBridgeRunning)
|
||||||
{
|
{
|
||||||
UnityMCPBridge.Stop();
|
UnityMcpBridge.Stop();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UnityMCPBridge.Start();
|
UnityMcpBridge.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
isUnityBridgeRunning = !isUnityBridgeRunning;
|
isUnityBridgeRunning = !isUnityBridgeRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private string GetPythonDirectory(List<string> possiblePaths)
|
private string GetPythonDirectory(List<string> possiblePaths)
|
||||||
{
|
{
|
||||||
foreach (var path in possiblePaths)
|
foreach (var path in possiblePaths)
|
||||||
{
|
{
|
||||||
// Skip wildcard paths for now
|
// Skip wildcard paths for now
|
||||||
if (path.Contains("*")) continue;
|
if (path.Contains("*"))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
|
|
@ -330,7 +399,8 @@ namespace UnityMCP.Editor.Windows
|
||||||
|
|
||||||
foreach (var path in possiblePaths)
|
foreach (var path in possiblePaths)
|
||||||
{
|
{
|
||||||
if (!path.Contains("*")) continue;
|
if (!path.Contains("*"))
|
||||||
|
continue;
|
||||||
|
|
||||||
string directoryPath = Path.GetDirectoryName(path);
|
string directoryPath = Path.GetDirectoryName(path);
|
||||||
string searchPattern = Path.GetFileName(Path.GetDirectoryName(path));
|
string searchPattern = Path.GetFileName(Path.GetDirectoryName(path));
|
||||||
|
|
@ -358,22 +428,13 @@ namespace UnityMCP.Editor.Windows
|
||||||
private string WriteToConfig(string pythonDir, string configPath)
|
private string WriteToConfig(string pythonDir, string configPath)
|
||||||
{
|
{
|
||||||
// Create configuration object for unityMCP
|
// Create configuration object for unityMCP
|
||||||
var unityMCPConfig = new MCPConfigServer
|
var unityMCPConfig = new McpConfigServer
|
||||||
{
|
{
|
||||||
command = "uv",
|
command = "uv",
|
||||||
args = new[]
|
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
||||||
{
|
|
||||||
"--directory",
|
|
||||||
pythonDir,
|
|
||||||
"run",
|
|
||||||
"server.py"
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var jsonSettings = new JsonSerializerSettings
|
var jsonSettings = new JsonSerializerSettings { Formatting = Formatting.Indented };
|
||||||
{
|
|
||||||
Formatting = Formatting.Indented
|
|
||||||
};
|
|
||||||
|
|
||||||
// Read existing config if it exists
|
// Read existing config if it exists
|
||||||
string existingJson = "{}";
|
string existingJson = "{}";
|
||||||
|
|
@ -403,7 +464,8 @@ namespace UnityMCP.Editor.Windows
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add/update unityMCP while preserving other servers
|
// Add/update unityMCP while preserving other servers
|
||||||
existingConfig.mcpServers.unityMCP = JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
|
existingConfig.mcpServers.unityMCP =
|
||||||
|
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
|
||||||
JsonConvert.SerializeObject(unityMCPConfig)
|
JsonConvert.SerializeObject(unityMCPConfig)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -428,28 +490,19 @@ namespace UnityMCP.Editor.Windows
|
||||||
string pythonDir = FindPackagePythonDirectory();
|
string pythonDir = FindPackagePythonDirectory();
|
||||||
|
|
||||||
// Create the manual configuration message
|
// Create the manual configuration message
|
||||||
var jsonConfig = new MCPConfig
|
var jsonConfig = new McpConfig
|
||||||
{
|
{
|
||||||
mcpServers = new MCPConfigServers
|
mcpServers = new McpConfigServers
|
||||||
{
|
{
|
||||||
unityMCP = new MCPConfigServer
|
unityMCP = new McpConfigServer
|
||||||
{
|
{
|
||||||
command = "uv",
|
command = "uv",
|
||||||
args = new[]
|
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
||||||
{
|
},
|
||||||
"--directory",
|
},
|
||||||
pythonDir,
|
|
||||||
"run",
|
|
||||||
"server.py"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var jsonSettings = new JsonSerializerSettings
|
var jsonSettings = new JsonSerializerSettings { Formatting = Formatting.Indented };
|
||||||
{
|
|
||||||
Formatting = Formatting.Indented
|
|
||||||
};
|
|
||||||
string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
|
string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
|
||||||
|
|
||||||
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
|
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
|
||||||
|
|
@ -474,8 +527,10 @@ namespace UnityMCP.Editor.Windows
|
||||||
string packagePath = package.resolvedPath;
|
string packagePath = package.resolvedPath;
|
||||||
string potentialPythonDir = Path.Combine(packagePath, "Python");
|
string potentialPythonDir = Path.Combine(packagePath, "Python");
|
||||||
|
|
||||||
if (Directory.Exists(potentialPythonDir) &&
|
if (
|
||||||
File.Exists(Path.Combine(potentialPythonDir, "server.py")))
|
Directory.Exists(potentialPythonDir)
|
||||||
|
&& File.Exists(Path.Combine(potentialPythonDir, "server.py"))
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return potentialPythonDir;
|
return potentialPythonDir;
|
||||||
}
|
}
|
||||||
|
|
@ -489,8 +544,9 @@ namespace UnityMCP.Editor.Windows
|
||||||
|
|
||||||
// If not found via Package Manager, try manual approaches
|
// If not found via Package Manager, try manual approaches
|
||||||
// First check for local installation
|
// First check for local installation
|
||||||
string[] possibleDirs = {
|
string[] possibleDirs =
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python"))
|
{
|
||||||
|
Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python")),
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var dir in possibleDirs)
|
foreach (var dir in possibleDirs)
|
||||||
|
|
@ -502,7 +558,9 @@ namespace UnityMCP.Editor.Windows
|
||||||
}
|
}
|
||||||
|
|
||||||
// If still not found, return the placeholder path
|
// If still not found, return the placeholder path
|
||||||
UnityEngine.Debug.LogWarning("Could not find Python directory, using placeholder path");
|
UnityEngine.Debug.LogWarning(
|
||||||
|
"Could not find Python directory, using placeholder path"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -523,7 +581,10 @@ namespace UnityMCP.Editor.Windows
|
||||||
{
|
{
|
||||||
configPath = mcpClient.windowsConfigPath;
|
configPath = mcpClient.windowsConfigPath;
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
else if (
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
|
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
configPath = mcpClient.linuxConfigPath;
|
configPath = mcpClient.linuxConfigPath;
|
||||||
}
|
}
|
||||||
|
|
@ -562,19 +623,26 @@ namespace UnityMCP.Editor.Windows
|
||||||
{
|
{
|
||||||
configPath = mcpClient.windowsConfigPath;
|
configPath = mcpClient.windowsConfigPath;
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
else if (
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
|
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
configPath = mcpClient.linuxConfigPath;
|
configPath = mcpClient.linuxConfigPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowManualInstructionsWindow(configPath, mcpClient);
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
||||||
UnityEngine.Debug.LogError($"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}");
|
UnityEngine.Debug.LogError(
|
||||||
|
$"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}"
|
||||||
|
);
|
||||||
return $"Failed to configure {mcpClient.name}";
|
return $"Failed to configure {mcpClient.name}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowCursorManualConfigurationInstructions(
|
||||||
private void ShowCursorManualConfigurationInstructions(string configPath, McpClient mcpClient)
|
string configPath,
|
||||||
|
McpClient mcpClient
|
||||||
|
)
|
||||||
{
|
{
|
||||||
mcpClient.SetStatus(McpStatus.Error, "Manual configuration required");
|
mcpClient.SetStatus(McpStatus.Error, "Manual configuration required");
|
||||||
|
|
||||||
|
|
@ -582,28 +650,19 @@ namespace UnityMCP.Editor.Windows
|
||||||
string pythonDir = FindPackagePythonDirectory();
|
string pythonDir = FindPackagePythonDirectory();
|
||||||
|
|
||||||
// Create the manual configuration message
|
// Create the manual configuration message
|
||||||
var jsonConfig = new MCPConfig
|
var jsonConfig = new McpConfig
|
||||||
{
|
{
|
||||||
mcpServers = new MCPConfigServers
|
mcpServers = new McpConfigServers
|
||||||
{
|
{
|
||||||
unityMCP = new MCPConfigServer
|
unityMCP = new McpConfigServer
|
||||||
{
|
{
|
||||||
command = "uv",
|
command = "uv",
|
||||||
args = new[]
|
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
||||||
{
|
},
|
||||||
"--directory",
|
},
|
||||||
pythonDir,
|
|
||||||
"run",
|
|
||||||
"server.py"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var jsonSettings = new JsonSerializerSettings
|
var jsonSettings = new JsonSerializerSettings { Formatting = Formatting.Indented };
|
||||||
{
|
|
||||||
Formatting = Formatting.Indented
|
|
||||||
};
|
|
||||||
string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
|
string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
|
||||||
|
|
||||||
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
|
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
|
||||||
|
|
@ -618,7 +677,10 @@ namespace UnityMCP.Editor.Windows
|
||||||
{
|
{
|
||||||
configPath = mcpClient.windowsConfigPath;
|
configPath = mcpClient.windowsConfigPath;
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
else if (
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
|
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
configPath = mcpClient.linuxConfigPath;
|
configPath = mcpClient.linuxConfigPath;
|
||||||
}
|
}
|
||||||
|
|
@ -635,12 +697,18 @@ namespace UnityMCP.Editor.Windows
|
||||||
}
|
}
|
||||||
|
|
||||||
string configJson = File.ReadAllText(configPath);
|
string configJson = File.ReadAllText(configPath);
|
||||||
var config = JsonConvert.DeserializeObject<MCPConfig>(configJson);
|
var config = JsonConvert.DeserializeObject<McpConfig>(configJson);
|
||||||
|
|
||||||
if (config?.mcpServers?.unityMCP != null)
|
if (config?.mcpServers?.unityMCP != null)
|
||||||
{
|
{
|
||||||
string pythonDir = GetPythonDirectory(possiblePaths);
|
string pythonDir = GetPythonDirectory(possiblePaths);
|
||||||
if (pythonDir != null && Array.Exists(config.mcpServers.unityMCP.args, arg => arg.Contains(pythonDir, StringComparison.Ordinal)))
|
if (
|
||||||
|
pythonDir != null
|
||||||
|
&& Array.Exists(
|
||||||
|
config.mcpServers.unityMCP.args,
|
||||||
|
arg => arg.Contains(pythonDir, StringComparison.Ordinal)
|
||||||
|
)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
mcpClient.SetStatus(McpStatus.Configured);
|
mcpClient.SetStatus(McpStatus.Configured);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "com.justinpbarnett.unity-mcp",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"displayName": "Unity MCP Bridge",
|
||||||
|
"description": "A bridge that manages and communicates with the sister application, Unity MCP Server, which allows for communications with MCP Clients like Claude Desktop or Cursor.",
|
||||||
|
"unity": "2020.3",
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.nuget.newtonsoft-json": "3.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "unity-mcp"
|
name = "unity-mcp-server"
|
||||||
version = "1.0.1"
|
version = "2.0.0"
|
||||||
description = "Unity MCP Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP)."
|
description = "Unity MCP Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP)."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
@ -12,7 +12,7 @@ logging.basicConfig(
|
||||||
level=getattr(logging, config.log_level),
|
level=getattr(logging, config.log_level),
|
||||||
format=config.log_format
|
format=config.log_format
|
||||||
)
|
)
|
||||||
logger = logging.getLogger("UnityMCP")
|
logger = logging.getLogger("unity-mcp-server")
|
||||||
|
|
||||||
# Global connection state
|
# Global connection state
|
||||||
_unity_connection: UnityConnection = None
|
_unity_connection: UnityConnection = None
|
||||||
|
|
@ -21,7 +21,7 @@ _unity_connection: UnityConnection = None
|
||||||
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
|
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
|
||||||
"""Handle server startup and shutdown."""
|
"""Handle server startup and shutdown."""
|
||||||
global _unity_connection
|
global _unity_connection
|
||||||
logger.info("UnityMCP server starting up")
|
logger.info("Unity MCP Server starting up")
|
||||||
try:
|
try:
|
||||||
_unity_connection = get_unity_connection()
|
_unity_connection = get_unity_connection()
|
||||||
logger.info("Connected to Unity on startup")
|
logger.info("Connected to Unity on startup")
|
||||||
|
|
@ -36,11 +36,11 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
|
||||||
if _unity_connection:
|
if _unity_connection:
|
||||||
_unity_connection.disconnect()
|
_unity_connection.disconnect()
|
||||||
_unity_connection = None
|
_unity_connection = None
|
||||||
logger.info("UnityMCP server shut down")
|
logger.info("Unity MCP Server shut down")
|
||||||
|
|
||||||
# Initialize MCP server
|
# Initialize MCP server
|
||||||
mcp = FastMCP(
|
mcp = FastMCP(
|
||||||
"UnityMCP",
|
"unity-mcp-server",
|
||||||
description="Unity Editor integration via Model Context Protocol",
|
description="Unity Editor integration via Model Context Protocol",
|
||||||
lifespan=server_lifespan
|
lifespan=server_lifespan
|
||||||
)
|
)
|
||||||
|
|
@ -8,7 +8,7 @@ from .execute_menu_item import register_execute_menu_item_tools
|
||||||
|
|
||||||
def register_all_tools(mcp):
|
def register_all_tools(mcp):
|
||||||
"""Register all refactored tools with the MCP server."""
|
"""Register all refactored tools with the MCP server."""
|
||||||
print("Registering UnityMCP refactored tools...")
|
print("Registering Unity MCP Server refactored tools...")
|
||||||
register_manage_script_tools(mcp)
|
register_manage_script_tools(mcp)
|
||||||
register_manage_scene_tools(mcp)
|
register_manage_scene_tools(mcp)
|
||||||
register_manage_editor_tools(mcp)
|
register_manage_editor_tools(mcp)
|
||||||
|
|
@ -16,4 +16,4 @@ def register_all_tools(mcp):
|
||||||
register_manage_asset_tools(mcp)
|
register_manage_asset_tools(mcp)
|
||||||
register_read_console_tools(mcp)
|
register_read_console_tools(mcp)
|
||||||
register_execute_menu_item_tools(mcp)
|
register_execute_menu_item_tools(mcp)
|
||||||
print("UnityMCP tool registration complete.")
|
print("Unity MCP Server tool registration complete.")
|
||||||
|
|
@ -10,7 +10,7 @@ logging.basicConfig(
|
||||||
level=getattr(logging, config.log_level),
|
level=getattr(logging, config.log_level),
|
||||||
format=config.log_format
|
format=config.log_format
|
||||||
)
|
)
|
||||||
logger = logging.getLogger("UnityMCP")
|
logger = logging.getLogger("unity-mcp-server")
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UnityConnection:
|
class UnityConnection:
|
||||||
|
|
@ -322,7 +322,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unity-mcp"
|
name = "unity-mcp"
|
||||||
version = "0.1.0"
|
version = "1.0.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
10
package.json
10
package.json
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"name": "com.justinpbarnett.unity-mcp",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"displayName": "Unity MCP",
|
|
||||||
"description": "A Unity package to communicate with a local MCP Client via a Python server.",
|
|
||||||
"unity": "2020.3",
|
|
||||||
"dependencies": {
|
|
||||||
"com.unity.nuget.newtonsoft-json": "3.0.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 1429c59739af3fc4d8b706950221d476
|
|
||||||
TextScriptImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
Loading…
Reference in New Issue