2026-06-06 20:12:40 +07:00
parent de84b2bf48
commit 97ac0f71f5
13682 changed files with 1125938 additions and 0 deletions
@@ -0,0 +1,215 @@
using UnityEditor;
using UnityEngine;
using SynapticAIPro;
using System.IO;
using System.Linq;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
namespace SynapticPro
{
/// <summary>
/// Detects installed Cinemachine version and defines appropriate scripting symbols
/// Supports both Cinemachine 2.x and 3.x
/// Automatically installs Unity.Splines dependency for Cinemachine 3.x
/// </summary>
[InitializeOnLoad]
public static class CinemachineVersionDetector
{
private const string CINEMACHINE_2_SYMBOL = "CINEMACHINE_2";
private const string CINEMACHINE_3_SYMBOL = "CINEMACHINE_3";
private const string CINEMACHINE_SYMBOL = "CINEMACHINE";
private const string SPLINES_PACKAGE = "com.unity.splines";
private static AddRequest splinesAddRequest;
static CinemachineVersionDetector()
{
DetectAndSetSymbols();
}
[MenuItem("Tools/Synaptic Pro/Detect Cinemachine Version")]
public static void DetectAndSetSymbols()
{
var cinemachineVersion = GetCinemachineVersion();
if (cinemachineVersion == null)
{
SynLog.Info("[Synaptic] Cinemachine not detected. Cinemachine features will be disabled.");
RemoveAllCinemachineSymbols();
return;
}
SynLog.Info($"[Synaptic] Detected Cinemachine version: {cinemachineVersion}");
// Parse version
var versionParts = cinemachineVersion.Split('.');
if (versionParts.Length > 0 && int.TryParse(versionParts[0], out int majorVersion))
{
if (majorVersion >= 3)
{
SetCinemachineSymbol(3);
SynLog.Info("[Synaptic] ✅ Cinemachine 3.x detected - Using Cinemachine 3 API");
// Cinemachine 3.x requires Unity.Splines package
CheckAndInstallSplines();
}
else if (majorVersion == 2)
{
SetCinemachineSymbol(2);
SynLog.Info("[Synaptic] ✅ Cinemachine 2.x detected - Using Cinemachine 2 API");
}
else
{
SynLog.Warn($"[Synaptic] ⚠️ Unsupported Cinemachine version: {cinemachineVersion}. Recommended: 2.9.7 or 3.0+");
RemoveAllCinemachineSymbols();
}
}
}
private static string GetCinemachineVersion()
{
// Check via PackageInfo
var request = UnityEditor.PackageManager.Client.List(true, false);
// Wait for completion (synchronous for InitializeOnLoad)
while (!request.IsCompleted)
{
System.Threading.Thread.Sleep(10);
}
if (request.Status == UnityEditor.PackageManager.StatusCode.Success)
{
var cinemachinePackage = request.Result.FirstOrDefault(p => p.name == "com.unity.cinemachine");
if (cinemachinePackage != null)
{
return cinemachinePackage.version;
}
}
// Fallback: Check if namespace exists via type checking
var cinemachine2Type = System.Type.GetType("Cinemachine.CinemachineVirtualCamera, Cinemachine");
var cinemachine3Type = System.Type.GetType("Unity.Cinemachine.CinemachineCamera, Unity.Cinemachine");
if (cinemachine3Type != null)
{
return "3.0.0"; // 3.x detected
}
else if (cinemachine2Type != null)
{
return "2.9.7"; // 2.x detected
}
return null; // Not installed
}
private static void SetCinemachineSymbol(int majorVersion)
{
var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
var definesList = defines.Split(';').ToList();
// Remove old symbols
definesList.Remove(CINEMACHINE_2_SYMBOL);
definesList.Remove(CINEMACHINE_3_SYMBOL);
definesList.Remove(CINEMACHINE_SYMBOL);
// Add appropriate symbols
definesList.Add(CINEMACHINE_SYMBOL);
if (majorVersion == 2)
{
definesList.Add(CINEMACHINE_2_SYMBOL);
}
else if (majorVersion == 3)
{
definesList.Add(CINEMACHINE_3_SYMBOL);
}
var newDefines = string.Join(";", definesList.Distinct().Where(s => !string.IsNullOrEmpty(s)));
PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, newDefines);
SynLog.Info($"[Synaptic] Scripting symbols updated: {newDefines}");
}
private static void RemoveAllCinemachineSymbols()
{
var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
var definesList = defines.Split(';').ToList();
definesList.Remove(CINEMACHINE_2_SYMBOL);
definesList.Remove(CINEMACHINE_3_SYMBOL);
definesList.Remove(CINEMACHINE_SYMBOL);
var newDefines = string.Join(";", definesList.Distinct().Where(s => !string.IsNullOrEmpty(s)));
PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, newDefines);
SynLog.Info("[Synaptic] Cinemachine symbols removed");
}
private static void CheckAndInstallSplines()
{
var listRequest = Client.List(true, false);
// Wait for completion (synchronous for simplicity)
while (!listRequest.IsCompleted)
{
System.Threading.Thread.Sleep(10);
}
if (listRequest.Status == StatusCode.Success)
{
bool isInstalled = false;
foreach (var package in listRequest.Result)
{
if (package.name == SPLINES_PACKAGE)
{
isInstalled = true;
SynLog.Info($"[Synaptic] Unity.Splines is already installed (version {package.version})");
break;
}
}
if (!isInstalled)
{
SynLog.Info("[Synaptic] Unity.Splines not found. Installing dependency for Cinemachine 3.x...");
InstallSplines();
}
}
else if (listRequest.Status >= StatusCode.Failure)
{
Debug.LogError($"[Synaptic] Failed to check Splines package: {listRequest.Error.message}");
}
}
private static void InstallSplines()
{
splinesAddRequest = Client.Add(SPLINES_PACKAGE);
EditorApplication.update += CheckSplinesInstallProgress;
}
private static void CheckSplinesInstallProgress()
{
if (splinesAddRequest == null || !splinesAddRequest.IsCompleted)
return;
EditorApplication.update -= CheckSplinesInstallProgress;
if (splinesAddRequest.Status == StatusCode.Success)
{
SynLog.Info($"[Synaptic] ✅ Successfully installed {SPLINES_PACKAGE} for Cinemachine 3.x");
SynLog.Info("[Synaptic] Please wait for Unity to recompile scripts...");
}
else if (splinesAddRequest.Status >= StatusCode.Failure)
{
Debug.LogError($"[Synaptic] ❌ Failed to install {SPLINES_PACKAGE}: {splinesAddRequest.Error.message}");
SynLog.Warn("[Synaptic] Cinemachine 3.x requires Unity.Splines package.\n" +
"Please install it manually via Package Manager:\n" +
"Window > Package Manager > + > Add package by name...\n" +
$"Package name: {SPLINES_PACKAGE}");
}
splinesAddRequest = null;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 5af59eb77df3e4503957595c272c14e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/CinemachineVersionDetector.cs
uploadId: 920982
@@ -0,0 +1,85 @@
using UnityEngine;
using SynapticAIPro;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
namespace SynapticPro
{
[InitializeOnLoad]
public static class NewtonsoftInstaller
{
private const string NEWTONSOFT_PACKAGE = "com.unity.nuget.newtonsoft-json";
private static AddRequest addRequest;
private static ListRequest listRequest;
static NewtonsoftInstaller()
{
// Check if Newtonsoft.Json is already installed
listRequest = Client.List();
EditorApplication.update += CheckListProgress;
}
private static void CheckListProgress()
{
if (listRequest == null || !listRequest.IsCompleted)
return;
EditorApplication.update -= CheckListProgress;
if (listRequest.Status == StatusCode.Success)
{
bool isInstalled = false;
foreach (var package in listRequest.Result)
{
if (package.name == NEWTONSOFT_PACKAGE)
{
isInstalled = true;
SynLog.Info($"[Synaptic AI Pro] Newtonsoft.Json is already installed (version {package.version})");
break;
}
}
if (!isInstalled)
{
SynLog.Info("[Synaptic AI Pro] Newtonsoft.Json not found. Installing...");
InstallNewtonsoftJson();
}
}
else if (listRequest.Status >= StatusCode.Failure)
{
Debug.LogError($"[Synaptic AI Pro] Failed to list packages: {listRequest.Error.message}");
}
listRequest = null;
}
private static void InstallNewtonsoftJson()
{
addRequest = Client.Add(NEWTONSOFT_PACKAGE);
EditorApplication.update += CheckInstallProgress;
}
private static void CheckInstallProgress()
{
if (addRequest == null || !addRequest.IsCompleted)
return;
EditorApplication.update -= CheckInstallProgress;
if (addRequest.Status == StatusCode.Success)
{
SynLog.Info($"[Synaptic AI Pro] Successfully installed {NEWTONSOFT_PACKAGE}");
}
else if (addRequest.Status >= StatusCode.Failure)
{
Debug.LogError($"[Synaptic AI Pro] Failed to install {NEWTONSOFT_PACKAGE}: {addRequest.Error.message}");
SynLog.Warn("[Synaptic AI Pro] Please install Newtonsoft.Json manually via Package Manager:\n" +
"Window > Package Manager > + > Add package by name...\n" +
"Package name: com.unity.nuget.newtonsoft-json");
}
addRequest = null;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 94dec97004834400dbc51f312324888c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NewtonsoftInstaller.cs
uploadId: 920982
@@ -0,0 +1,54 @@
{
"name": "Synaptic.MCP.Unity.Editor",
"rootNamespace": "SynapticPro",
"references": [
"Synaptic.MCP.Unity",
"Unity.Mathematics",
"Unity.Collections",
"Unity.Burst",
"Unity.TextMeshPro",
"Unity.Nuget.Newtonsoft-Json",
"Unity.ugui",
"Cinemachine",
"Unity.Cinemachine",
"Unity.Splines",
"Unity.VisualEffectGraph.Editor"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [
{
"name": "com.unity.cinemachine",
"expression": "[3.0,4.0)",
"define": "CINEMACHINE_3"
},
{
"name": "com.unity.cinemachine",
"expression": "[2.0,3.0)",
"define": "CINEMACHINE_2"
},
{
"name": "com.unity.cinemachine",
"expression": "",
"define": "CINEMACHINE"
},
{
"name": "com.unity.visualeffectgraph",
"expression": "",
"define": "VFX_GRAPH_PACKAGE"
},
{
"name": "com.unity.visualeffectgraph",
"expression": "[10.0,)",
"define": "VFX_GRAPH_10_PLUS"
}
],
"noEngineReferences": false
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 343c464e684b044469a954b748d0e625
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusAI.MCP.Unity.Editor.asmdef
uploadId: 920982
@@ -0,0 +1,801 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using SynapticAIPro;
namespace SynapticPro
{
/// <summary>
/// Helper class for advanced animation and motion management
/// Handles Mixamo integration, IK setup, and animation asset management
/// </summary>
public static class NexusAnimationHelper
{
private const string MIXAMO_RIG_PREFIX = "mixamorig:";
private const string HUMANOID_AVATAR_PREFIX = "Avatar_";
/// <summary>
/// Import and setup Mixamo FBX with automatic Humanoid configuration
/// </summary>
public static string ImportMixamoAnimation(Dictionary<string, string> parameters)
{
try
{
string fbxPath = parameters.GetValueOrDefault("fbxPath", "");
string targetPath = parameters.GetValueOrDefault("targetPath", "Assets/Animations/Mixamo/");
bool createController = parameters.GetValueOrDefault("createController", "true") == "true";
string characterName = parameters.GetValueOrDefault("characterName", "Character");
bool setupIK = parameters.GetValueOrDefault("setupIK", "true") == "true";
if (string.IsNullOrEmpty(fbxPath) || !File.Exists(fbxPath))
{
return $"Error: FBX file not found at path: {fbxPath}";
}
// Ensure target directory exists
if (!AssetDatabase.IsValidFolder(targetPath))
{
string[] folders = targetPath.Split('/');
string currentPath = folders[0];
for (int i = 1; i < folders.Length; i++)
{
if (string.IsNullOrEmpty(folders[i])) continue;
string nextPath = currentPath + "/" + folders[i];
if (!AssetDatabase.IsValidFolder(nextPath))
{
AssetDatabase.CreateFolder(currentPath, folders[i]);
}
currentPath = nextPath;
}
}
// Copy FBX to project
string fileName = Path.GetFileName(fbxPath);
string assetPath = Path.Combine(targetPath, fileName);
File.Copy(fbxPath, assetPath, true);
AssetDatabase.Refresh();
// Configure as Humanoid
ModelImporter importer = AssetImporter.GetAtPath(assetPath) as ModelImporter;
if (importer != null)
{
// Setup for Mixamo
importer.animationType = ModelImporterAnimationType.Human;
importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
// Animation settings
importer.importAnimation = true;
importer.animationCompression = ModelImporterAnimationCompression.Optimal;
// Optimize for Mixamo
importer.optimizeGameObjects = true;
importer.optimizeMeshPolygons = true;
importer.optimizeMeshVertices = true;
// Material settings
importer.materialImportMode = ModelImporterMaterialImportMode.ImportStandard;
// Apply settings
importer.SaveAndReimport();
var result = new Dictionary<string, object>
{
["success"] = true,
["assetPath"] = assetPath,
["characterName"] = characterName
};
// Create Animator Controller if requested
if (createController)
{
string controllerPath = CreateAnimatorControllerForMixamo(assetPath, characterName, targetPath);
result["controllerPath"] = controllerPath;
}
// Setup IK if requested
if (setupIK)
{
SetupBasicIK(characterName);
result["ikSetup"] = true;
}
return JsonUtility.ToJson(result);
}
return "Error: Failed to configure model importer";
}
catch (Exception e)
{
return $"Error importing Mixamo animation: {e.Message}";
}
}
/// <summary>
/// Organize and categorize animation clips
/// </summary>
public static string OrganizeAnimationAssets(Dictionary<string, string> parameters)
{
try
{
string sourcePath = parameters.GetValueOrDefault("sourcePath", "Assets/Animations/");
bool autoDetectType = parameters.GetValueOrDefault("autoDetectType", "true") == "true";
bool createFolders = parameters.GetValueOrDefault("createFolders", "true") == "true";
// Find all animation clips
string[] guids = AssetDatabase.FindAssets("t:AnimationClip", new[] { sourcePath });
var categories = new Dictionary<string, List<AnimationClip>>
{
["Idle"] = new List<AnimationClip>(),
["Walk"] = new List<AnimationClip>(),
["Run"] = new List<AnimationClip>(),
["Jump"] = new List<AnimationClip>(),
["Attack"] = new List<AnimationClip>(),
["Death"] = new List<AnimationClip>(),
["Damage"] = new List<AnimationClip>(),
["Other"] = new List<AnimationClip>()
};
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
if (clip != null && autoDetectType)
{
string category = DetectAnimationType(clip.name);
categories[category].Add(clip);
// Move to categorized folder if requested
if (createFolders)
{
string targetFolder = Path.Combine(sourcePath, category);
if (!AssetDatabase.IsValidFolder(targetFolder))
{
AssetDatabase.CreateFolder(sourcePath, category);
}
string newPath = Path.Combine(targetFolder, Path.GetFileName(path));
if (path != newPath)
{
AssetDatabase.MoveAsset(path, newPath);
}
}
}
}
// Generate report
var report = new Dictionary<string, object>
{
["totalClips"] = guids.Length,
["categories"] = new Dictionary<string, int>()
};
foreach (var category in categories)
{
((Dictionary<string, int>)report["categories"])[category.Key] = category.Value.Count;
}
return JsonUtility.ToJson(report);
}
catch (Exception e)
{
return $"Error organizing animation assets: {e.Message}";
}
}
/// <summary>
/// Setup IK for character
/// </summary>
public static string SetupCharacterIK(Dictionary<string, string> parameters)
{
try
{
string gameObjectName = parameters.GetValueOrDefault("gameObject", "");
bool enableFootIK = parameters.GetValueOrDefault("enableFootIK", "true") == "true";
bool enableHandIK = parameters.GetValueOrDefault("enableHandIK", "false") == "true";
bool enableLookAt = parameters.GetValueOrDefault("enableLookAt", "false") == "true";
float footIKWeight = float.Parse(parameters.GetValueOrDefault("footIKWeight", "1"));
float handIKWeight = float.Parse(parameters.GetValueOrDefault("handIKWeight", "1"));
GameObject target = GameObject.Find(gameObjectName);
if (target == null)
{
return $"Error: GameObject '{gameObjectName}' not found";
}
// Add IK controller component if not exists
var ikController = target.GetComponent<IKController>();
if (ikController == null)
{
ikController = target.AddComponent<IKController>();
Undo.RegisterCreatedObjectUndo(ikController, "Add IK Controller");
}
// Configure IK settings
ikController.enableFootIK = enableFootIK;
ikController.enableHandIK = enableHandIK;
ikController.enableLookAt = enableLookAt;
ikController.footIKWeight = footIKWeight;
ikController.handIKWeight = handIKWeight;
// Setup IK targets
if (enableFootIK)
{
CreateIKTarget(target.transform, "LeftFootIK", new Vector3(-0.1f, 0, 0));
CreateIKTarget(target.transform, "RightFootIK", new Vector3(0.1f, 0, 0));
}
if (enableHandIK)
{
CreateIKTarget(target.transform, "LeftHandIK", new Vector3(-0.5f, 1.5f, 0.5f));
CreateIKTarget(target.transform, "RightHandIK", new Vector3(0.5f, 1.5f, 0.5f));
}
if (enableLookAt)
{
CreateIKTarget(target.transform, "LookAtTarget", new Vector3(0, 1.6f, 2f));
}
EditorUtility.SetDirty(target);
return JsonUtility.ToJson(new Dictionary<string, object>
{
["success"] = true,
["gameObject"] = gameObjectName,
["footIK"] = enableFootIK,
["handIK"] = enableHandIK,
["lookAt"] = enableLookAt
});
}
catch (Exception e)
{
return $"Error setting up IK: {e.Message}";
}
}
/// <summary>
/// Create animation layer mask
/// </summary>
public static string CreateAnimationLayerMask(Dictionary<string, string> parameters)
{
try
{
string maskName = parameters.GetValueOrDefault("maskName", "NewLayerMask");
string savePath = parameters.GetValueOrDefault("savePath", "Assets/Animations/Masks/");
string includeBonesPattern = parameters.GetValueOrDefault("includeBones", "");
string excludeBonesPattern = parameters.GetValueOrDefault("excludeBones", "");
string avatarPath = parameters.GetValueOrDefault("avatarPath", "");
if (!AssetDatabase.IsValidFolder(savePath))
{
Directory.CreateDirectory(savePath);
AssetDatabase.Refresh();
}
// Create avatar mask
AvatarMask mask = new AvatarMask();
mask.name = maskName;
// Load avatar if specified
if (!string.IsNullOrEmpty(avatarPath))
{
Avatar avatar = AssetDatabase.LoadAssetAtPath<Avatar>(avatarPath);
if (avatar != null)
{
// Configure body parts based on patterns
ConfigureAvatarMaskBodyParts(mask, includeBonesPattern, excludeBonesPattern);
}
}
string assetPath = Path.Combine(savePath, maskName + ".mask");
AssetDatabase.CreateAsset(mask, assetPath);
AssetDatabase.SaveAssets();
return JsonUtility.ToJson(new Dictionary<string, object>
{
["success"] = true,
["maskPath"] = assetPath,
["maskName"] = maskName
});
}
catch (Exception e)
{
return $"Error creating layer mask: {e.Message}";
}
}
/// <summary>
/// Setup animation blend tree
/// </summary>
public static string SetupAdvancedBlendTree(Dictionary<string, string> parameters)
{
try
{
string controllerPath = parameters.GetValueOrDefault("controllerPath", "");
string blendType = parameters.GetValueOrDefault("blendType", "2D");
string parameterX = parameters.GetValueOrDefault("parameterX", "MoveX");
string parameterY = parameters.GetValueOrDefault("parameterY", "MoveY");
string clips = parameters.GetValueOrDefault("clips", ""); // Comma separated paths
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
if (controller == null)
{
return "Error: Animator Controller not found";
}
// Create blend tree
var rootStateMachine = controller.layers[0].stateMachine;
var blendTreeState = rootStateMachine.AddState("BlendTree");
BlendTree blendTree;
controller.CreateBlendTreeInController("Movement", out blendTree);
blendTreeState.motion = blendTree;
// Configure blend type
switch (blendType.ToLower())
{
case "1d":
blendTree.blendType = BlendTreeType.Simple1D;
blendTree.blendParameter = parameterX;
break;
case "2d":
blendTree.blendType = BlendTreeType.SimpleDirectional2D;
blendTree.blendParameter = parameterX;
blendTree.blendParameterY = parameterY;
break;
case "freeform":
blendTree.blendType = BlendTreeType.FreeformDirectional2D;
blendTree.blendParameter = parameterX;
blendTree.blendParameterY = parameterY;
break;
}
// Add animation clips
if (!string.IsNullOrEmpty(clips))
{
string[] clipPaths = clips.Split(',');
foreach (string clipPath in clipPaths)
{
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath.Trim());
if (clip != null)
{
blendTree.AddChild(clip);
}
}
}
// Add parameters if not exist
if (!controller.parameters.Any(p => p.name == parameterX))
{
controller.AddParameter(parameterX, AnimatorControllerParameterType.Float);
}
if (!string.IsNullOrEmpty(parameterY) && !controller.parameters.Any(p => p.name == parameterY))
{
controller.AddParameter(parameterY, AnimatorControllerParameterType.Float);
}
EditorUtility.SetDirty(controller);
AssetDatabase.SaveAssets();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(controller), ImportAssetOptions.ForceUpdate);
return JsonUtility.ToJson(new Dictionary<string, object>
{
["success"] = true,
["blendTreeName"] = "Movement",
["blendType"] = blendType,
["clipCount"] = blendTree.children.Length
});
}
catch (Exception e)
{
return $"Error setting up blend tree: {e.Message}";
}
}
/// <summary>
/// Retarget animation from one rig to another
/// </summary>
public static string RetargetAnimation(Dictionary<string, string> parameters)
{
try
{
string sourceClipPath = parameters.GetValueOrDefault("sourceClip", "");
string targetAvatarPath = parameters.GetValueOrDefault("targetAvatar", "");
string outputPath = parameters.GetValueOrDefault("outputPath", "Assets/Animations/Retargeted/");
bool adjustRootMotion = parameters.GetValueOrDefault("adjustRootMotion", "true") == "true";
AnimationClip sourceClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(sourceClipPath);
Avatar targetAvatar = AssetDatabase.LoadAssetAtPath<Avatar>(targetAvatarPath);
if (sourceClip == null || targetAvatar == null)
{
return "Error: Source clip or target avatar not found";
}
// Create output directory
if (!AssetDatabase.IsValidFolder(outputPath))
{
Directory.CreateDirectory(outputPath);
AssetDatabase.Refresh();
}
// Clone animation clip
AnimationClip retargetedClip = UnityEngine.Object.Instantiate(sourceClip);
retargetedClip.name = sourceClip.name + "_Retargeted";
// Adjust root motion if needed
if (adjustRootMotion)
{
AnimationClipSettings settings = AnimationUtility.GetAnimationClipSettings(retargetedClip);
settings.loopTime = sourceClip.isLooping;
settings.keepOriginalPositionY = true;
settings.keepOriginalOrientation = true;
AnimationUtility.SetAnimationClipSettings(retargetedClip, settings);
}
string savePath = Path.Combine(outputPath, retargetedClip.name + ".anim");
AssetDatabase.CreateAsset(retargetedClip, savePath);
AssetDatabase.SaveAssets();
return JsonUtility.ToJson(new Dictionary<string, object>
{
["success"] = true,
["retargetedClip"] = savePath,
["originalClip"] = sourceClipPath
});
}
catch (Exception e)
{
return $"Error retargeting animation: {e.Message}";
}
}
/// <summary>
/// Create animation transition presets
/// </summary>
public static string CreateTransitionPreset(Dictionary<string, string> parameters)
{
try
{
string controllerPath = parameters.GetValueOrDefault("controllerPath", "");
string fromState = parameters.GetValueOrDefault("fromState", "");
string toState = parameters.GetValueOrDefault("toState", "");
string presetType = parameters.GetValueOrDefault("presetType", "smooth"); // smooth, instant, crossfade
float duration = float.Parse(parameters.GetValueOrDefault("duration", "0.25"));
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
if (controller == null)
{
return "Error: Controller not found";
}
var stateMachine = controller.layers[0].stateMachine;
var sourceState = FindState(stateMachine, fromState);
var destState = FindState(stateMachine, toState);
if (sourceState == null || destState == null)
{
return "Error: States not found";
}
var transition = sourceState.AddTransition(destState);
// Configure based on preset
switch (presetType.ToLower())
{
case "instant":
transition.duration = 0;
transition.offset = 0;
transition.hasExitTime = false;
break;
case "smooth":
transition.duration = duration;
transition.offset = 0;
transition.hasExitTime = true;
transition.exitTime = 0.75f;
break;
case "crossfade":
transition.duration = duration * 2;
transition.offset = 0.1f;
transition.hasExitTime = true;
transition.exitTime = 0.5f;
break;
}
EditorUtility.SetDirty(controller);
AssetDatabase.SaveAssets();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(controller), ImportAssetOptions.ForceUpdate);
return JsonUtility.ToJson(new Dictionary<string, object>
{
["success"] = true,
["fromState"] = fromState,
["toState"] = toState,
["presetType"] = presetType
});
}
catch (Exception e)
{
return $"Error creating transition preset: {e.Message}";
}
}
/// <summary>
/// Analyze animation performance
/// </summary>
public static string AnalyzeAnimationPerformance(Dictionary<string, string> parameters)
{
try
{
string targetPath = parameters.GetValueOrDefault("targetPath", "Assets/Animations/");
string[] guids = AssetDatabase.FindAssets("t:AnimationClip", new[] { targetPath });
var report = new Dictionary<string, object>
{
["totalClips"] = guids.Length,
["clips"] = new List<Dictionary<string, object>>()
};
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
if (clip != null)
{
var clipInfo = new Dictionary<string, object>
{
["name"] = clip.name,
["length"] = clip.length,
["frameRate"] = clip.frameRate,
["isHumanMotion"] = clip.humanMotion,
["isLooping"] = clip.isLooping,
["events"] = clip.events.Length,
["approximateSize"] = EstimateAnimationSize(clip)
};
((List<Dictionary<string, object>>)report["clips"]).Add(clipInfo);
}
}
return JsonUtility.ToJson(report);
}
catch (Exception e)
{
return $"Error analyzing animation performance: {e.Message}";
}
}
// ===== Helper Methods =====
private static string CreateAnimatorControllerForMixamo(string fbxPath, string characterName, string targetPath)
{
string controllerPath = Path.Combine(targetPath, characterName + "_Controller.controller");
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
// Create parameters
controller.AddParameter("Speed", AnimatorControllerParameterType.Float);
controller.AddParameter("Direction", AnimatorControllerParameterType.Float);
controller.AddParameter("IsGrounded", AnimatorControllerParameterType.Bool);
controller.AddParameter("Jump", AnimatorControllerParameterType.Trigger);
// Create default states
var stateMachine = controller.layers[0].stateMachine;
var idleState = stateMachine.AddState("Idle");
stateMachine.defaultState = idleState;
// Try to find and assign idle animation from the FBX
var clips = AssetDatabase.LoadAllAssetsAtPath(fbxPath).OfType<AnimationClip>().ToArray();
foreach (var clip in clips)
{
if (clip.name.ToLower().Contains("idle"))
{
idleState.motion = clip;
break;
}
}
EditorUtility.SetDirty(controller);
AssetDatabase.SaveAssets();
AssetDatabase.ImportAsset(controllerPath, ImportAssetOptions.ForceUpdate);
return controllerPath;
}
private static void SetupBasicIK(string characterName)
{
// This would normally set up IK components
// Actual implementation would depend on the IK solution being used
SynLog.Info($"IK setup prepared for {characterName}");
}
private static string DetectAnimationType(string clipName)
{
string lowerName = clipName.ToLower();
if (lowerName.Contains("idle") || lowerName.Contains("stand"))
return "Idle";
if (lowerName.Contains("walk"))
return "Walk";
if (lowerName.Contains("run") || lowerName.Contains("sprint"))
return "Run";
if (lowerName.Contains("jump") || lowerName.Contains("leap"))
return "Jump";
if (lowerName.Contains("attack") || lowerName.Contains("punch") || lowerName.Contains("kick"))
return "Attack";
if (lowerName.Contains("death") || lowerName.Contains("die"))
return "Death";
if (lowerName.Contains("damage") || lowerName.Contains("hit") || lowerName.Contains("hurt"))
return "Damage";
return "Other";
}
private static GameObject CreateIKTarget(Transform parent, string name, Vector3 localPosition)
{
GameObject ikTarget = new GameObject(name);
ikTarget.transform.SetParent(parent);
ikTarget.transform.localPosition = localPosition;
// Add visual gizmo component for editor
var gizmo = ikTarget.AddComponent<IKTargetGizmo>();
gizmo.color = name.Contains("Foot") ? Color.green : (name.Contains("Hand") ? Color.blue : Color.yellow);
Undo.RegisterCreatedObjectUndo(ikTarget, $"Create {name}");
return ikTarget;
}
private static void ConfigureAvatarMaskBodyParts(AvatarMask mask, string includeBones, string excludeBones)
{
// Configure humanoid body parts
for (int i = 0; i < (int)AvatarMaskBodyPart.LastBodyPart; i++)
{
bool include = true;
AvatarMaskBodyPart part = (AvatarMaskBodyPart)i;
string partName = part.ToString().ToLower();
if (!string.IsNullOrEmpty(excludeBones) && excludeBones.ToLower().Contains(partName))
{
include = false;
}
if (!string.IsNullOrEmpty(includeBones) && !includeBones.ToLower().Contains(partName))
{
include = false;
}
mask.SetHumanoidBodyPartActive(part, include);
}
}
private static AnimatorState FindState(AnimatorStateMachine stateMachine, string name)
{
foreach (var state in stateMachine.states)
{
if (state.state.name == name)
return state.state;
}
return null;
}
private static float EstimateAnimationSize(AnimationClip clip)
{
// Rough estimation based on curves and keys
var bindings = AnimationUtility.GetCurveBindings(clip);
int totalKeys = 0;
foreach (var binding in bindings)
{
var curve = AnimationUtility.GetEditorCurve(clip, binding);
if (curve != null)
{
totalKeys += curve.keys.Length;
}
}
// Approximate size in KB (4 bytes per float * keys * 4 values per key)
return (totalKeys * 4 * 4) / 1024f;
}
}
/// <summary>
/// IK Controller component for runtime IK handling
/// </summary>
public class IKController : MonoBehaviour
{
public bool enableFootIK = true;
public bool enableHandIK = false;
public bool enableLookAt = false;
public float footIKWeight = 1f;
public float handIKWeight = 1f;
public float lookAtWeight = 1f;
public Transform leftFootTarget;
public Transform rightFootTarget;
public Transform leftHandTarget;
public Transform rightHandTarget;
public Transform lookAtTarget;
private Animator animator;
void Start()
{
animator = GetComponent<Animator>();
}
void OnAnimatorIK(int layerIndex)
{
if (animator == null) return;
// Foot IK
if (enableFootIK)
{
if (leftFootTarget != null)
{
animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, footIKWeight);
animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, footIKWeight);
animator.SetIKPosition(AvatarIKGoal.LeftFoot, leftFootTarget.position);
animator.SetIKRotation(AvatarIKGoal.LeftFoot, leftFootTarget.rotation);
}
if (rightFootTarget != null)
{
animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, footIKWeight);
animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, footIKWeight);
animator.SetIKPosition(AvatarIKGoal.RightFoot, rightFootTarget.position);
animator.SetIKRotation(AvatarIKGoal.RightFoot, rightFootTarget.rotation);
}
}
// Hand IK
if (enableHandIK)
{
if (leftHandTarget != null)
{
animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, handIKWeight);
animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, handIKWeight);
animator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandTarget.position);
animator.SetIKRotation(AvatarIKGoal.LeftHand, leftHandTarget.rotation);
}
if (rightHandTarget != null)
{
animator.SetIKPositionWeight(AvatarIKGoal.RightHand, handIKWeight);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand, handIKWeight);
animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandTarget.position);
animator.SetIKRotation(AvatarIKGoal.RightHand, rightHandTarget.rotation);
}
}
// Look At
if (enableLookAt && lookAtTarget != null)
{
animator.SetLookAtWeight(lookAtWeight);
animator.SetLookAtPosition(lookAtTarget.position);
}
}
}
/// <summary>
/// Visual gizmo for IK targets
/// </summary>
public class IKTargetGizmo : MonoBehaviour
{
public Color color = Color.green;
public float size = 0.1f;
void OnDrawGizmos()
{
Gizmos.color = color;
Gizmos.DrawWireSphere(transform.position, size);
Gizmos.DrawLine(transform.position, transform.position + transform.forward * size * 2);
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 12ea05feed0164e43b479cec9d83a119
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusAnimationHelper.cs
uploadId: 920982
@@ -0,0 +1,728 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEditor;
using System.Text;
using Newtonsoft.Json;
namespace SynapticPro
{
/// <summary>
/// Detailed asset information analysis class
/// Retrieves detailed information for textures, meshes, audio, animations, etc.
/// </summary>
public static class NexusAssetAnalyzer
{
/// <summary>
/// Get detailed information for texture assets
/// </summary>
public static string GetTextureDetails(Dictionary<string, string> parameters)
{
try
{
var textureName = parameters.GetValueOrDefault("textureName", "");
var includeAll = parameters.GetValueOrDefault("includeAll", "false") == "true";
var includeMipmaps = parameters.GetValueOrDefault("includeMipmaps", "true") == "true";
var includeCompressionInfo = parameters.GetValueOrDefault("includeCompressionInfo", "true") == "true";
var includeMemoryUsage = parameters.GetValueOrDefault("includeMemoryUsage", "true") == "true";
var textures = string.IsNullOrEmpty(textureName) && includeAll ?
Resources.FindObjectsOfTypeAll<Texture2D>() :
Resources.FindObjectsOfTypeAll<Texture2D>().Where(t =>
string.IsNullOrEmpty(textureName) || t.name.Contains(textureName)).ToArray();
var textureDetails = new Dictionary<string, object>
{
["total_count"] = textures.Length,
["search_criteria"] = new Dictionary<string, object>
{
["texture_name"] = textureName,
["include_all"] = includeAll,
["include_mipmaps"] = includeMipmaps,
["include_compression_info"] = includeCompressionInfo,
["include_memory_usage"] = includeMemoryUsage
},
["textures"] = textures.Take(20).Select(texture => {
if (texture == null) return null;
var textureData = new Dictionary<string, object>
{
["name"] = texture.name,
["size"] = new Dictionary<string, int>
{
["width"] = texture.width,
["height"] = texture.height
},
["format"] = texture.format.ToString(),
["filter_mode"] = texture.filterMode.ToString(),
["wrap_mode"] = texture.wrapMode.ToString(),
["is_readable"] = texture.isReadable
};
if (includeMipmaps)
{
textureData["mipmap_info"] = new Dictionary<string, object>
{
["mipmap_count"] = texture.mipmapCount,
["has_mipmaps"] = texture.mipmapCount > 1
};
}
if (includeMemoryUsage)
{
var memorySize = UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(texture);
textureData["memory_usage"] = new Dictionary<string, object>
{
["bytes"] = memorySize,
["formatted"] = FormatBytes(memorySize)
};
}
if (includeCompressionInfo)
{
var assetPath = AssetDatabase.GetAssetPath(texture);
if (!string.IsNullOrEmpty(assetPath))
{
var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (importer != null)
{
textureData["import_settings"] = new Dictionary<string, object>
{
["asset_path"] = assetPath,
["texture_type"] = importer.textureType.ToString(),
["max_texture_size"] = importer.maxTextureSize,
["compression"] = importer.textureCompression.ToString(),
["srgb"] = importer.sRGBTexture
};
}
}
}
return textureData;
}).Where(t => t != null).ToList()
};
return JsonConvert.SerializeObject(textureDetails, Formatting.Indented);
}
catch (Exception e)
{
return $"Error analyzing textures: {e.Message}";
}
}
/// <summary>
/// Get detailed information for mesh assets
/// </summary>
public static string GetMeshDetails(Dictionary<string, string> parameters)
{
try
{
var meshName = parameters.GetValueOrDefault("meshName", "");
var includeAll = parameters.GetValueOrDefault("includeAll", "false") == "true";
var includeVertexData = parameters.GetValueOrDefault("includeVertexData", "true") == "true";
var includeSubMeshes = parameters.GetValueOrDefault("includeSubMeshes", "true") == "true";
var includeBoneWeights = parameters.GetValueOrDefault("includeBoneWeights", "true") == "true";
var includeBlendShapes = parameters.GetValueOrDefault("includeBlendShapes", "false") == "true";
var meshes = string.IsNullOrEmpty(meshName) && includeAll ?
Resources.FindObjectsOfTypeAll<Mesh>() :
Resources.FindObjectsOfTypeAll<Mesh>().Where(m =>
string.IsNullOrEmpty(meshName) || m.name.Contains(meshName)).ToArray();
var report = new StringBuilder();
report.AppendLine("=== Mesh Details ===");
report.AppendLine($"Found {meshes.Length} mesh(es)");
report.AppendLine();
foreach (var mesh in meshes.Take(20))
{
if (mesh == null) continue;
report.AppendLine($"Mesh: {mesh.name}");
if (includeVertexData)
{
report.AppendLine($" Vertex Count: {mesh.vertexCount:N0}");
report.AppendLine($" Triangle Count: {mesh.triangles.Length / 3:N0}");
report.AppendLine($" UV Channels: {GetUVChannelCount(mesh)}");
report.AppendLine($" Has Normals: {mesh.normals.Length > 0}");
report.AppendLine($" Has Tangents: {mesh.tangents.Length > 0}");
report.AppendLine($" Has Colors: {mesh.colors.Length > 0}");
}
if (includeSubMeshes)
{
report.AppendLine($" SubMesh Count: {mesh.subMeshCount}");
for (int i = 0; i < mesh.subMeshCount; i++)
{
var subMesh = mesh.GetSubMesh(i);
report.AppendLine($" SubMesh {i}: {subMesh.indexCount / 3:N0} triangles");
}
}
if (includeBoneWeights)
{
var boneWeights = mesh.boneWeights;
report.AppendLine($" Bone Weights: {boneWeights.Length}");
report.AppendLine($" Bind Poses: {mesh.bindposes.Length}");
}
if (includeBlendShapes)
{
report.AppendLine($" Blend Shapes: {mesh.blendShapeCount}");
for (int i = 0; i < mesh.blendShapeCount; i++)
{
report.AppendLine($" {mesh.GetBlendShapeName(i)}: {mesh.GetBlendShapeFrameCount(i)} frames");
}
}
var memorySize = UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(mesh);
report.AppendLine($" Memory Usage: {FormatBytes(memorySize)}");
report.AppendLine($" Readable: {mesh.isReadable}");
report.AppendLine();
}
return report.ToString();
}
catch (Exception e)
{
return $"Error analyzing meshes: {e.Message}";
}
}
/// <summary>
/// Get detailed information for audio assets
/// </summary>
public static string GetAudioDetails(Dictionary<string, string> parameters)
{
try
{
var audioName = parameters.GetValueOrDefault("audioName", "");
var includeAll = parameters.GetValueOrDefault("includeAll", "false") == "true";
var includeCompressionInfo = parameters.GetValueOrDefault("includeCompressionInfo", "true") == "true";
var includeMetadata = parameters.GetValueOrDefault("includeMetadata", "true") == "true";
var audioClips = string.IsNullOrEmpty(audioName) && includeAll ?
Resources.FindObjectsOfTypeAll<AudioClip>() :
Resources.FindObjectsOfTypeAll<AudioClip>().Where(a =>
string.IsNullOrEmpty(audioName) || a.name.Contains(audioName)).ToArray();
var report = new StringBuilder();
report.AppendLine("=== Audio Clip Details ===");
report.AppendLine($"Found {audioClips.Length} audio clip(s)");
report.AppendLine();
foreach (var clip in audioClips.Take(20))
{
if (clip == null) continue;
report.AppendLine($"Audio Clip: {clip.name}");
report.AppendLine($" Length: {clip.length:F2} seconds");
report.AppendLine($" Channels: {clip.channels}");
report.AppendLine($" Frequency: {clip.frequency} Hz");
report.AppendLine($" Samples: {clip.samples:N0}");
report.AppendLine($" Load Type: {clip.loadType}");
report.AppendLine($" 3D: {!clip.ambisonic}");
if (includeCompressionInfo)
{
var assetPath = AssetDatabase.GetAssetPath(clip);
if (!string.IsNullOrEmpty(assetPath))
{
var importer = AssetImporter.GetAtPath(assetPath) as AudioImporter;
if (importer != null)
{
report.AppendLine($" Force Mono: {importer.forceToMono}");
var defaultSettings = importer.defaultSampleSettings;
report.AppendLine($" Preload Audio Data: {defaultSettings.loadType == AudioClipLoadType.CompressedInMemory}");
var settings = importer.defaultSampleSettings;
report.AppendLine($" Compression Format: {settings.compressionFormat}");
report.AppendLine($" Quality: {settings.quality}");
report.AppendLine($" Sample Rate: {settings.sampleRateSetting}");
}
}
}
var memorySize = UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(clip);
report.AppendLine($" Memory Usage: {FormatBytes(memorySize)}");
if (includeMetadata)
{
var fileInfo = GetAssetFileInfo(AssetDatabase.GetAssetPath(clip));
if (fileInfo != null)
{
report.AppendLine($" File Size: {FormatBytes(fileInfo.Length)}");
}
}
report.AppendLine();
}
return report.ToString();
}
catch (Exception e)
{
return $"Error analyzing audio clips: {e.Message}";
}
}
/// <summary>
/// Get detailed information for animation assets
/// </summary>
public static string GetAnimationDetails(Dictionary<string, string> parameters)
{
try
{
var animationName = parameters.GetValueOrDefault("animationName", "");
var includeAll = parameters.GetValueOrDefault("includeAll", "false") == "true";
var includeKeyframes = parameters.GetValueOrDefault("includeKeyframes", "true") == "true";
var includeEvents = parameters.GetValueOrDefault("includeEvents", "true") == "true";
var animationClips = string.IsNullOrEmpty(animationName) && includeAll ?
Resources.FindObjectsOfTypeAll<AnimationClip>() :
Resources.FindObjectsOfTypeAll<AnimationClip>().Where(a =>
string.IsNullOrEmpty(animationName) || a.name.Contains(animationName)).ToArray();
var report = new StringBuilder();
report.AppendLine("=== Animation Clip Details ===");
report.AppendLine($"Found {animationClips.Length} animation clip(s)");
report.AppendLine();
foreach (var clip in animationClips.Take(20))
{
if (clip == null) continue;
report.AppendLine($"Animation Clip: {clip.name}");
report.AppendLine($" Length: {clip.length:F3} seconds");
report.AppendLine($" Frame Rate: {clip.frameRate:F1} fps");
report.AppendLine($" Legacy: {clip.legacy}");
report.AppendLine($" Loop: {clip.isLooping}");
report.AppendLine($" Humanoid: {clip.isHumanMotion}");
if (includeEvents)
{
var events = AnimationUtility.GetAnimationEvents(clip);
report.AppendLine($" Animation Events: {events.Length}");
foreach (var evt in events.Take(5))
{
report.AppendLine($" {evt.time:F2}s: {evt.functionName}({evt.stringParameter})");
}
}
if (includeKeyframes)
{
var bindings = AnimationUtility.GetCurveBindings(clip);
report.AppendLine($" Curve Bindings: {bindings.Length}");
int totalKeyframes = 0;
foreach (var binding in bindings.Take(10))
{
var curve = AnimationUtility.GetEditorCurve(clip, binding);
if (curve != null)
{
totalKeyframes += curve.keys.Length;
report.AppendLine($" {binding.propertyName}: {curve.keys.Length} keys");
}
}
report.AppendLine($" Total Keyframes: {totalKeyframes:N0}");
}
var memorySize = UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(clip);
report.AppendLine($" Memory Usage: {FormatBytes(memorySize)}");
report.AppendLine();
}
return report.ToString();
}
catch (Exception e)
{
return $"Error analyzing animation clips: {e.Message}";
}
}
/// <summary>
/// Get detailed information for material assets
/// </summary>
public static string GetMaterialDetails(Dictionary<string, string> parameters)
{
try
{
var materialName = parameters.GetValueOrDefault("materialName", "");
var includeAll = parameters.GetValueOrDefault("includeAll", "false") == "true";
var includeShaderInfo = parameters.GetValueOrDefault("includeShaderInfo", "true") == "true";
var includeTextureReferences = parameters.GetValueOrDefault("includeTextureReferences", "true") == "true";
var includePropertyValues = parameters.GetValueOrDefault("includePropertyValues", "true") == "true";
var materials = string.IsNullOrEmpty(materialName) && includeAll ?
Resources.FindObjectsOfTypeAll<Material>() :
Resources.FindObjectsOfTypeAll<Material>().Where(m =>
string.IsNullOrEmpty(materialName) || m.name.Contains(materialName)).ToArray();
var report = new StringBuilder();
report.AppendLine("=== Material Details ===");
report.AppendLine($"Found {materials.Length} material(s)");
report.AppendLine();
foreach (var material in materials.Take(20))
{
if (material == null || material.shader == null) continue;
report.AppendLine($"Material: {material.name}");
if (includeShaderInfo)
{
report.AppendLine($" Shader: {material.shader.name}");
report.AppendLine($" Render Queue: {material.renderQueue}");
report.AppendLine($" Keywords: {string.Join(", ", material.shaderKeywords)}");
}
if (includeTextureReferences)
{
var texturePropertyNames = material.GetTexturePropertyNames();
report.AppendLine($" Textures ({texturePropertyNames.Length}):");
foreach (var propName in texturePropertyNames)
{
var texture = material.GetTexture(propName);
if (texture != null)
{
report.AppendLine($" {propName}: {texture.name} ({texture.width}x{texture.height})");
}
}
}
if (includePropertyValues)
{
// Color properties
var colorProps = GetShaderColorProperties(material.shader);
foreach (var prop in colorProps.Take(5))
{
var color = material.GetColor(prop);
report.AppendLine($" {prop}: RGBA({color.r:F2}, {color.g:F2}, {color.b:F2}, {color.a:F2})");
}
// Float properties
var floatProps = GetShaderFloatProperties(material.shader);
foreach (var prop in floatProps.Take(5))
{
var value = material.GetFloat(prop);
report.AppendLine($" {prop}: {value:F3}");
}
}
var memorySize = UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(material);
report.AppendLine($" Memory Usage: {FormatBytes(memorySize)}");
report.AppendLine();
}
return report.ToString();
}
catch (Exception e)
{
return $"Error analyzing materials: {e.Message}";
}
}
/// <summary>
/// Get asset file information
/// </summary>
public static string GetAssetFileInfo(Dictionary<string, string> parameters)
{
try
{
var assetPath = parameters.GetValueOrDefault("assetPath", "");
var assetType = parameters.GetValueOrDefault("assetType", "all");
var includeImportSettings = parameters.GetValueOrDefault("includeImportSettings", "true") == "true";
var includeMetadata = parameters.GetValueOrDefault("includeMetadata", "true") == "true";
var sortBy = parameters.GetValueOrDefault("sortBy", "name");
var assetGuids = string.IsNullOrEmpty(assetPath) ?
AssetDatabase.FindAssets(GetAssetTypeFilter(assetType)) :
new[] { AssetDatabase.AssetPathToGUID(assetPath) };
var assetInfos = new List<AssetFileInfo>();
foreach (var guid in assetGuids.Take(100)) // Limit for performance
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(path)) continue;
var fileInfo = GetAssetFileInfo(path);
if (fileInfo != null)
{
var assetInfo = new AssetFileInfo
{
Path = path,
Name = Path.GetFileName(path),
Size = fileInfo.Length,
CreationTime = fileInfo.CreationTime,
LastWriteTime = fileInfo.LastWriteTime,
Extension = Path.GetExtension(path)
};
if (includeImportSettings)
{
var importer = AssetImporter.GetAtPath(path);
assetInfo.ImporterType = importer?.GetType().Name ?? "Unknown";
}
assetInfos.Add(assetInfo);
}
}
// Sort results
assetInfos = SortAssetInfos(assetInfos, sortBy);
return FormatAssetFileReport(assetInfos, includeMetadata);
}
catch (Exception e)
{
return $"Error getting asset file info: {e.Message}";
}
}
/// <summary>
/// Analyze asset usage
/// </summary>
public static string AnalyzeAssetUsage(Dictionary<string, string> parameters)
{
try
{
var assetType = parameters.GetValueOrDefault("assetType", "all");
var findUnused = parameters.GetValueOrDefault("findUnused", "true") == "true";
var findDuplicates = parameters.GetValueOrDefault("findDuplicates", "false") == "true";
var includeSceneReferences = parameters.GetValueOrDefault("includeSceneReferences", "true") == "true";
var includePrefabReferences = parameters.GetValueOrDefault("includePrefabReferences", "true") == "true";
var report = new StringBuilder();
report.AppendLine("=== Asset Usage Analysis ===");
var assetGuids = AssetDatabase.FindAssets(GetAssetTypeFilter(assetType));
report.AppendLine($"Analyzing {assetGuids.Length} assets of type '{assetType}'");
report.AppendLine();
if (findUnused)
{
var unusedAssets = FindUnusedAssets(assetGuids, includeSceneReferences, includePrefabReferences);
report.AppendLine($"=== Unused Assets ({unusedAssets.Count}) ===");
foreach (var asset in unusedAssets.Take(20))
{
report.AppendLine($" {asset}");
}
report.AppendLine();
}
if (findDuplicates)
{
var duplicates = FindPotentialDuplicates(assetGuids);
report.AppendLine($"=== Potential Duplicates ({duplicates.Count}) ===");
foreach (var group in duplicates.Take(10))
{
report.AppendLine($" Similar names:");
foreach (var asset in group)
{
report.AppendLine($" {asset}");
}
report.AppendLine();
}
}
return report.ToString();
}
catch (Exception e)
{
return $"Error analyzing asset usage: {e.Message}";
}
}
// ===== Helper Methods =====
private static int GetUVChannelCount(Mesh mesh)
{
int count = 0;
var uvs = new List<Vector2>();
for (int i = 0; i < 8; i++)
{
mesh.GetUVs(i, uvs);
if (uvs.Count > 0) count++;
uvs.Clear();
}
return count;
}
private static string[] GetShaderColorProperties(Shader shader)
{
var properties = new List<string>();
int count = ShaderUtil.GetPropertyCount(shader);
for (int i = 0; i < count; i++)
{
if (ShaderUtil.GetPropertyType(shader, i) == ShaderUtil.ShaderPropertyType.Color)
{
properties.Add(ShaderUtil.GetPropertyName(shader, i));
}
}
return properties.ToArray();
}
private static string[] GetShaderFloatProperties(Shader shader)
{
var properties = new List<string>();
int count = ShaderUtil.GetPropertyCount(shader);
for (int i = 0; i < count; i++)
{
var type = ShaderUtil.GetPropertyType(shader, i);
if (type == ShaderUtil.ShaderPropertyType.Float || type == ShaderUtil.ShaderPropertyType.Range)
{
properties.Add(ShaderUtil.GetPropertyName(shader, i));
}
}
return properties.ToArray();
}
private static FileInfo GetAssetFileInfo(string assetPath)
{
try
{
var fullPath = Path.Combine(Application.dataPath, assetPath.Substring(7)); // Remove "Assets/"
return new FileInfo(fullPath);
}
catch
{
return null;
}
}
private static string GetAssetTypeFilter(string assetType)
{
return assetType switch
{
"textures" => "t:Texture2D",
"meshes" => "t:Mesh",
"audio" => "t:AudioClip",
"scripts" => "t:MonoScript",
"prefabs" => "t:Prefab",
"materials" => "t:Material",
_ => ""
};
}
private static List<AssetFileInfo> SortAssetInfos(List<AssetFileInfo> assetInfos, string sortBy)
{
return sortBy switch
{
"size" => assetInfos.OrderByDescending(a => a.Size).ToList(),
"date" => assetInfos.OrderByDescending(a => a.LastWriteTime).ToList(),
"type" => assetInfos.OrderBy(a => a.Extension).ToList(),
_ => assetInfos.OrderBy(a => a.Name).ToList()
};
}
private static List<string> FindUnusedAssets(string[] assetGuids, bool includeScenes, bool includePrefabs)
{
var unusedAssets = new List<string>();
var dependencies = AssetDatabase.GetDependencies(AssetDatabase.GetAllAssetPaths(), true);
foreach (var guid in assetGuids.Take(50)) // Limit for performance
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(path)) continue;
// Simple check - if asset is not in dependencies, it might be unused
if (!dependencies.Contains(path))
{
unusedAssets.Add(path);
}
}
return unusedAssets;
}
private static List<List<string>> FindPotentialDuplicates(string[] assetGuids)
{
var duplicates = new List<List<string>>();
var nameGroups = new Dictionary<string, List<string>>();
foreach (var guid in assetGuids.Take(100))
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(path)) continue;
var name = Path.GetFileNameWithoutExtension(path).ToLower();
if (!nameGroups.ContainsKey(name))
{
nameGroups[name] = new List<string>();
}
nameGroups[name].Add(path);
}
foreach (var group in nameGroups.Values)
{
if (group.Count > 1)
{
duplicates.Add(group);
}
}
return duplicates;
}
private static string FormatAssetFileReport(List<AssetFileInfo> assetInfos, bool includeMetadata)
{
var report = new StringBuilder();
report.AppendLine("=== Asset File Information ===");
report.AppendLine($"Found {assetInfos.Count} asset(s)");
report.AppendLine();
long totalSize = 0;
foreach (var asset in assetInfos.Take(50))
{
report.AppendLine($"Asset: {asset.Name}");
report.AppendLine($" Path: {asset.Path}");
report.AppendLine($" Size: {FormatBytes(asset.Size)}");
report.AppendLine($" Type: {asset.Extension}");
if (includeMetadata)
{
report.AppendLine($" Created: {asset.CreationTime:yyyy-MM-dd HH:mm:ss}");
report.AppendLine($" Modified: {asset.LastWriteTime:yyyy-MM-dd HH:mm:ss}");
report.AppendLine($" Importer: {asset.ImporterType}");
}
totalSize += asset.Size;
report.AppendLine();
}
report.AppendLine($"Total Size: {FormatBytes(totalSize)}");
return report.ToString();
}
private static string FormatBytes(long bytes)
{
string[] suffixes = { "B", "KB", "MB", "GB" };
int counter = 0;
decimal number = (decimal)bytes;
while (Math.Round(number / 1024) >= 1)
{
number = number / 1024;
counter++;
}
return string.Format("{0:n1} {1}", number, suffixes[counter]);
}
private class AssetFileInfo
{
public string Path { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public DateTime CreationTime { get; set; }
public DateTime LastWriteTime { get; set; }
public string Extension { get; set; }
public string ImporterType { get; set; }
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 639c91d87c2894f878fe7f014cd43212
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusAssetAnalyzer.cs
uploadId: 920982
@@ -0,0 +1,583 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEditor;
using Newtonsoft.Json;
namespace SynapticPro
{
/// <summary>
/// Arbitrary C# evaluation for the Editor — equivalent of Blender's
/// run_python tool. Wraps Mono.CSharp.Evaluator (instance API) so callers
/// can execute any C# snippet against the running Editor without
/// triggering an AssemblyReload.
///
/// Static Evaluator.Init/Run on Unity 2022.3+ silently no-ops; the real
/// path is `new Evaluator(new CompilerContext(new CompilerSettings(),
/// new ConsoleReportPrinter()))` plus injecting every assembly already
/// loaded in the AppDomain so UnityEngine / UnityEditor / Newtonsoft.Json
/// resolve.
///
/// Expressions ("1+1", "GameObject.Find(\"X\").name") must NOT end with
/// a semicolon — those are evaluated via Evaluate(...). Statements
/// ("var x = 1; Debug.Log(x);") run through Run(...).
/// </summary>
public static class NexusCSharpEval
{
private static object _evaluator;
private static MethodInfo _evaluateMethod;
private static MethodInfo _runMethod;
private static MethodInfo _referenceAssemblyMethod;
private static StringBuilder _captured = new StringBuilder();
private static readonly object _lock = new object();
// ESC-0107 fix (E): capture Unity Debug.Log output that Console.SetOut
// can't catch (UnityEngine.Debug routes through Unity Console, not
// managed Console.Out). Subscribe to Application.logMessageReceived
// while a Run call is in progress.
private static bool _captureUnityLogs = false;
private static Application.LogCallback _logCallback;
// ESC-0107 fix (D, revised): receive the return value from `return X;`
// snippets via a static field. The user's `return X;` is rewritten to
// `SynapticPro.NexusCSharpEval.__SetResult(X);` and executed through
// Evaluator.Run, which lets Mono.CSharp's normal method-body parser
// handle the expression (no pointer-type ambiguity, no Evaluate
// restrictions). We then read the field back in managed code.
public static object __LastResult;
public static bool __LastResultSet;
public static void __SetResult(object value)
{
__LastResult = value;
__LastResultSet = true;
}
public static string Run(Dictionary<string, string> parameters)
{
var code = parameters != null && parameters.TryGetValue("code", out var c) ? c : "";
if (string.IsNullOrEmpty(code))
{
return JsonConvert.SerializeObject(new
{
success = false,
error = "code parameter is required",
example = "GameObject.Find(\"Cube\")?.name"
});
}
lock (_lock)
{
try
{
if (!EnsureInitialized(out var initError))
{
return JsonConvert.SerializeObject(new
{
success = false,
error = initError
});
}
_captured.Length = 0;
var oldOut = Console.Out;
Console.SetOut(new StringWriter(_captured));
// Hook Unity Debug.Log into the capture buffer (ESC-0107 fix E).
AttachUnityLogCapture();
try
{
// ESC-0107 fix D (revised again): rewrite the trailing
// `return X;` into a call to our static `__SetResult(X)`
// sink, then Run the whole thing as statements. This
// sidesteps two Mono.CSharp quirks:
// - Run(...) discards real `return` values
// - Evaluate("x * 1") mis-parses var*literal as a
// pointer-type declaration, returning resultSet=false
// Inside a normal Run body the parser treats `*` as
// unambiguous multiplication and `__SetResult(X)` as a
// regular method call. The receiver field is read back
// in managed code after Run completes.
var trimmed = code.TrimEnd();
SplitReturnTail(trimmed, out var prefixStatements, out var returnExpression);
// For bare expressions (no `;`) we still use Evaluate
// — it works fine for that case and avoids a needless
// method-call wrap.
string expressionToEvaluate = null;
string rewrittenStatements = null;
if (returnExpression != null)
{
rewrittenStatements = string.IsNullOrEmpty(prefixStatements)
? $"SynapticPro.NexusCSharpEval.__SetResult({returnExpression});"
: $"{prefixStatements} SynapticPro.NexusCSharpEval.__SetResult({returnExpression});";
}
else if (!trimmed.EndsWith(";"))
{
expressionToEvaluate = code;
}
// Reset the sink before every call so a stale value
// from a previous run can't leak through.
__LastResult = null;
__LastResultSet = false;
if (rewrittenStatements != null && _runMethod != null)
{
// First try plain Run. Works for most snippets and
// is the cheapest path. If it succeeds but doesn't
// set the result (e.g. the user's code contains
// generic type syntax `List<int>` which Mono.CSharp
// Evaluator's top-level parser chokes on), retry
// wrapped in an immediately-invoked Action lambda
// — Mono parses the lambda body with the regular
// method-body parser, which handles generics fine.
object runResultR;
try { runResultR = _runMethod.Invoke(_evaluator, new object[] { rewrittenStatements }); }
catch (TargetInvocationException tie)
{
return Error(tie.InnerException ?? tie);
}
bool runOkR = runResultR is bool rbR && rbR;
if (!__LastResultSet)
{
// Reset for the wrapped retry (output already
// captured what the failed attempt printed,
// which should be nothing — Run prints nothing
// on parse failure).
__LastResult = null;
__LastResultSet = false;
var wrapped = $"((System.Action)(() => {{ {rewrittenStatements} }}))();";
try { runResultR = _runMethod.Invoke(_evaluator, new object[] { wrapped }); }
catch (TargetInvocationException tie)
{
return Error(tie.InnerException ?? tie);
}
runOkR = runResultR is bool rbR2 && rbR2;
}
return JsonConvert.SerializeObject(new
{
success = runOkR,
output = _captured.ToString(),
result = __LastResultSet ? SafeStringify(__LastResult) : null,
resultSet = __LastResultSet
});
}
if (expressionToEvaluate != null && _evaluateMethod != null)
{
var args = new object[] { expressionToEvaluate, null, false };
object remainderObj = null;
try { remainderObj = _evaluateMethod.Invoke(_evaluator, args); }
catch (TargetInvocationException tie)
{
return Error(tie.InnerException ?? tie);
}
string remainder = remainderObj as string ?? "";
object result = args[1];
bool resultSet = args[2] is bool b && b;
if (string.IsNullOrEmpty(remainder))
{
return JsonConvert.SerializeObject(new
{
success = true,
output = _captured.ToString(),
result = resultSet ? SafeStringify(result) : null,
resultSet
});
}
// Evaluator returned remainder — the "expression" we
// extracted was actually parsed as statements. Fall
// through to statement-only path for the full input.
}
// Pure-statement input (no return form, no bare expression)
// — fall through to plain Run. Captures stdout but no
// value. Note: with the lambda-wrap approach above we
// never executed the prefix as a side effect already,
// so the original `code` is safe to Run here as-is.
// Statement mode.
if (_runMethod == null)
{
return JsonConvert.SerializeObject(new
{
success = false,
error = "Evaluator.Run method not found on this Mono.CSharp build"
});
}
object runResult;
try { runResult = _runMethod.Invoke(_evaluator, new object[] { code }); }
catch (TargetInvocationException tie)
{
return Error(tie.InnerException ?? tie);
}
bool runOk = runResult is bool rb && rb;
// Also surface __SetResult writes from user code, even
// when we didn't auto-rewrite. Lets advanced callers do
// their own SetResult invocation (e.g. inside lambdas
// when avoiding the Evaluator generic-parse bug).
if (__LastResultSet)
{
return JsonConvert.SerializeObject(new
{
success = runOk,
output = _captured.ToString(),
result = SafeStringify(__LastResult),
resultSet = true
});
}
return JsonConvert.SerializeObject(new
{
success = runOk,
output = _captured.ToString(),
result = (object)null
});
}
finally
{
Console.SetOut(oldOut);
DetachUnityLogCapture();
}
}
catch (Exception e)
{
return Error(e);
}
}
}
private static bool EnsureInitialized(out string error)
{
error = null;
if (_evaluator != null) return true;
Assembly mcs = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "Mono.CSharp");
if (mcs == null)
{
try { mcs = Assembly.Load("Mono.CSharp"); } catch { /* try alt below */ }
}
if (mcs == null)
{
error = "Mono.CSharp.dll is not loaded in this Unity build. " +
"Add an .asmdef reference to Mono.CSharp or use a Unity " +
"version that bundles it (most Unity LTS releases do).";
return false;
}
Type settingsType = mcs.GetType("Mono.CSharp.CompilerSettings");
Type printerType = mcs.GetType("Mono.CSharp.ConsoleReportPrinter");
Type reportType = mcs.GetType("Mono.CSharp.Report");
Type contextType = mcs.GetType("Mono.CSharp.CompilerContext");
Type evalType = mcs.GetType("Mono.CSharp.Evaluator");
if (settingsType == null || printerType == null || contextType == null || evalType == null)
{
error = "Mono.CSharp internal types missing " +
$"(settings={settingsType != null}, printer={printerType != null}, " +
$"context={contextType != null}, eval={evalType != null}).";
return false;
}
object settings = Activator.CreateInstance(settingsType);
object printer = Activator.CreateInstance(printerType);
// Try CompilerContext(CompilerSettings, ReportPrinter)
object ctx = null;
ConstructorInfo ctxCtor = contextType.GetConstructors()
.FirstOrDefault(ci => ci.GetParameters().Length == 2);
if (ctxCtor != null)
{
try { ctx = ctxCtor.Invoke(new object[] { settings, printer }); }
catch { ctx = null; }
}
if (ctx == null)
{
error = "Could not construct Mono.CSharp.CompilerContext.";
return false;
}
ConstructorInfo evalCtor = evalType.GetConstructor(new[] { contextType });
if (evalCtor == null)
{
error = "Mono.CSharp.Evaluator(CompilerContext) constructor not found.";
return false;
}
_evaluator = evalCtor.Invoke(new object[] { ctx });
_evaluateMethod = evalType.GetMethod(
"Evaluate",
BindingFlags.Public | BindingFlags.Instance,
null,
new[] { typeof(string), typeof(object).MakeByRefType(), typeof(bool).MakeByRefType() },
null);
_runMethod = evalType.GetMethod(
"Run",
BindingFlags.Public | BindingFlags.Instance,
null,
new[] { typeof(string) },
null);
_referenceAssemblyMethod = evalType.GetMethod(
"ReferenceAssembly",
BindingFlags.Public | BindingFlags.Instance,
null,
new[] { typeof(Assembly) },
null);
// Inject every already-loaded assembly so user code can reach
// UnityEngine / UnityEditor / Newtonsoft.Json / the project's
// own scripts without manual `using`.
if (_referenceAssemblyMethod != null)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
if (asm.IsDynamic) continue;
if (string.IsNullOrEmpty(asm.Location)) continue;
_referenceAssemblyMethod.Invoke(_evaluator, new object[] { asm });
}
catch { /* skip individual failures */ }
}
}
// Pre-import common namespaces.
if (_runMethod != null)
{
try
{
_runMethod.Invoke(_evaluator, new object[]
{
"using System; " +
"using System.Collections.Generic; " +
"using System.Linq; " +
"using System.IO; " +
"using UnityEngine; " +
"using UnityEditor; " +
"using Newtonsoft.Json;"
});
}
catch { /* best-effort */ }
}
return true;
}
private static string Error(Exception e)
{
return JsonConvert.SerializeObject(new
{
success = false,
error = e.Message,
stackTrace = e.StackTrace
});
}
private static object SafeStringify(object value)
{
if (value == null) return null;
try
{
var t = value.GetType();
if (t.IsPrimitive || value is string) return value;
if (typeof(UnityEngine.Object).IsAssignableFrom(t)) return value.ToString();
return JsonConvert.SerializeObject(value);
}
catch
{
return value?.ToString();
}
}
/// <summary>
/// Split `[statements...] return X;` into (prefix, expression).
/// Locates the LAST top-level `return` keyword (depth 0 from braces,
/// parens, brackets, strings and comments) and splits there.
///
/// Earlier implementations split on the last top-level `;` and then
/// checked that the resulting final statement started with `return`.
/// That broke on inputs like `foreach (var x in xs) { X(); } return Y;`
/// because the only top-level `;` is the trailing one (the `X();`
/// inside braces is at depth 1) so the "final statement" became the
/// entire body including the foreach — not starting with `return`.
///
/// The new approach: scan for `return` itself at depth 0, take
/// everything before it as the prefix (its predecessor will end in
/// `;` or `}` — both are valid statement terminators that Mono.CSharp
/// Evaluator.Run accepts), take everything between `return` and the
/// trailing `;` as the expression.
/// </summary>
private static void SplitReturnTail(string trimmed, out string prefix, out string expression)
{
prefix = "";
expression = null;
if (string.IsNullOrEmpty(trimmed)) return;
if (!trimmed.EndsWith(";")) return;
var withoutSemi = trimmed.Substring(0, trimmed.Length - 1).TrimEnd();
int returnIdx = FindLastTopLevelReturnKeyword(withoutSemi);
if (returnIdx < 0) return;
// Expression is the slice after the `return` keyword.
var expr = withoutSemi.Substring(returnIdx + "return".Length).Trim();
if (string.IsNullOrEmpty(expr)) return;
expression = expr;
if (returnIdx > 0)
{
// Prefix is everything before `return`. Validate it terminates
// with `;` or `}` — anything else (e.g. half-written input)
// would be malformed, in which case skip the rewrite.
var candidatePrefix = withoutSemi.Substring(0, returnIdx).TrimEnd();
if (!candidatePrefix.EndsWith(";") && !candidatePrefix.EndsWith("}"))
{
expression = null;
return;
}
prefix = candidatePrefix;
}
}
/// <summary>
/// Find the start index of the LAST occurrence of the `return`
/// keyword in <paramref name="s"/> that sits at the top level
/// (outside any (), [], {} group, and outside string/char/comment
/// literals). Also enforces token boundaries so `Return`,
/// `myReturn`, `returnX` etc. don't match.
///
/// Returns -1 when no such keyword exists.
/// </summary>
private static int FindLastTopLevelReturnKeyword(string s)
{
const string KW = "return";
int paren = 0, bracket = 0, brace = 0;
int lastReturn = -1;
int i = 0;
while (i < s.Length)
{
char c = s[i];
// Line comment // ...
if (c == '/' && i + 1 < s.Length && s[i + 1] == '/')
{
while (i < s.Length && s[i] != '\n') i++;
continue;
}
// Block comment /* ... */
if (c == '/' && i + 1 < s.Length && s[i + 1] == '*')
{
i += 2;
while (i + 1 < s.Length && !(s[i] == '*' && s[i + 1] == '/')) i++;
i += 2;
continue;
}
// String / verbatim / interpolated — skip until closing quote.
if (c == '"' || c == '\'')
{
char quote = c;
bool verbatim = i > 0 && (s[i - 1] == '@' || s[i - 1] == '$');
i++;
while (i < s.Length)
{
if (!verbatim && s[i] == '\\') { i += 2; continue; }
if (s[i] == quote)
{
if (verbatim && i + 1 < s.Length && s[i + 1] == quote) { i += 2; continue; }
i++;
break;
}
i++;
}
continue;
}
if (c == '(') { paren++; i++; continue; }
if (c == ')') { paren--; i++; continue; }
if (c == '[') { bracket++; i++; continue; }
if (c == ']') { bracket--; i++; continue; }
if (c == '{') { brace++; i++; continue; }
if (c == '}') { brace--; i++; continue; }
// Check for `return` keyword start at this position.
if (paren == 0 && bracket == 0 && brace == 0 &&
c == KW[0] && i + KW.Length <= s.Length &&
s.Substring(i, KW.Length) == KW)
{
// Left boundary: must be start of string OR a non-identifier
// char (whitespace, `}`, `;`, `{`, etc.).
bool leftOk = (i == 0) || !IsIdentChar(s[i - 1]);
// Right boundary: must be whitespace or `(` after the keyword.
bool rightOk = false;
if (i + KW.Length < s.Length)
{
var nxt = s[i + KW.Length];
rightOk = !IsIdentChar(nxt);
}
else
{
rightOk = true; // EOF immediately after — `return` with no expr (handled later)
}
if (leftOk && rightOk)
{
lastReturn = i;
i += KW.Length;
continue;
}
}
i++;
}
return lastReturn;
}
private static bool IsIdentChar(char c)
{
return c == '_' || char.IsLetterOrDigit(c);
}
private static void AttachUnityLogCapture()
{
if (_captureUnityLogs) return;
_captureUnityLogs = true;
_logCallback = (string condition, string stackTrace, LogType type) =>
{
// Mirror Debug.Log / LogWarning / LogError into _captured so the
// caller sees what their script printed (NexusCSharpEval is the
// only writer to _captured during a Run call, no contention).
try
{
var prefix = type == LogType.Error || type == LogType.Exception ? "[error] "
: type == LogType.Warning ? "[warn] "
: "";
_captured.Append(prefix).Append(condition).Append('\n');
}
catch { /* best-effort, never throw from log hook */ }
};
Application.logMessageReceived += _logCallback;
}
private static void DetachUnityLogCapture()
{
if (!_captureUnityLogs) return;
try
{
if (_logCallback != null) Application.logMessageReceived -= _logCallback;
}
catch { }
_captureUnityLogs = false;
_logCallback = null;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 4807fcd26b9e2497388d1c548cb48f8e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusCSharpEval.cs
uploadId: 920982
@@ -0,0 +1,746 @@
using UnityEditor;
using UnityEngine;
using SynapticAIPro;
namespace SynapticPro
{
/// <summary>
/// Changelog dialog shown on first import or version update
/// </summary>
[InitializeOnLoad]
public class NexusChangelogWindow : EditorWindow
{
private static string CURRENT_VERSION => NexusVersion.Current;
private const string PREF_KEY_LAST_VERSION = "SynapticPro_LastShownVersion";
private const string PREF_KEY_DONT_SHOW = "SynapticPro_DontShowChangelog";
private const string PREF_KEY_LANGUAGE = "SynapticPro_ChangelogLanguage";
private enum Language { English, Japanese }
private static Language currentLanguage = Language.English;
private static bool dontShowAgain = false;
private Vector2 scrollPosition;
static NexusChangelogWindow()
{
EditorApplication.delayCall += ShowOnStartupIfNeeded;
}
private static void ShowOnStartupIfNeeded()
{
// Don't show during play mode
if (EditorApplication.isPlayingOrWillChangePlaymode)
return;
// Check if user disabled
if (EditorPrefs.GetBool(PREF_KEY_DONT_SHOW, false))
return;
// Check if already shown for this version
string lastVersion = EditorPrefs.GetString(PREF_KEY_LAST_VERSION, "");
if (lastVersion == CURRENT_VERSION)
return;
// Show dialog
ShowWindow();
// Mark as shown
EditorPrefs.SetString(PREF_KEY_LAST_VERSION, CURRENT_VERSION);
}
[MenuItem("Tools/Synaptic Pro/What's New", false, 100)]
public static void ShowWindow()
{
var window = GetWindow<NexusChangelogWindow>(true, "Synaptic AI Pro - What's New", true);
window.minSize = new Vector2(500, 450);
window.maxSize = new Vector2(600, 650);
window.ShowUtility();
}
private void OnEnable()
{
dontShowAgain = EditorPrefs.GetBool(PREF_KEY_DONT_SHOW, false);
currentLanguage = (Language)EditorPrefs.GetInt(PREF_KEY_LANGUAGE, 0);
}
private void OnGUI()
{
// Language selector (top right)
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUILayout.Label(currentLanguage == Language.English ? "Language:" : "言語:", GUILayout.Width(60));
var newLang = (Language)EditorGUILayout.Popup((int)currentLanguage, new string[] { "English", "日本語" }, GUILayout.Width(80));
if (newLang != currentLanguage)
{
currentLanguage = newLang;
EditorPrefs.SetInt(PREF_KEY_LANGUAGE, (int)currentLanguage);
}
EditorGUILayout.EndHorizontal();
// Header
GUILayout.Space(5);
var headerStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 20,
alignment = TextAnchor.MiddleCenter
};
GUILayout.Label($"Synaptic AI Pro v{CURRENT_VERSION}", headerStyle);
GUILayout.Space(5);
var subtitleStyle = new GUIStyle(EditorStyles.label)
{
alignment = TextAnchor.MiddleCenter,
fontStyle = FontStyle.Italic
};
GUILayout.Label(L("What's New", "更新内容"), subtitleStyle);
GUILayout.Space(15);
// Changelog content
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
DrawChangelogContent();
EditorGUILayout.EndScrollView();
GUILayout.Space(10);
// Don't show again toggle
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
bool newDontShow = EditorGUILayout.ToggleLeft(
L("Don't show on startup", "起動時に表示しない"),
dontShowAgain, GUILayout.Width(180));
if (newDontShow != dontShowAgain)
{
dontShowAgain = newDontShow;
EditorPrefs.SetBool(PREF_KEY_DONT_SHOW, dontShowAgain);
}
EditorGUILayout.EndHorizontal();
GUILayout.Space(5);
// Buttons
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button(L("Open Setup", "セットアップを開く"), GUILayout.Width(120), GUILayout.Height(30)))
{
NexusMCPSetupWindow.ShowWindow();
Close();
}
GUILayout.Space(10);
if (GUILayout.Button(L("Close", "閉じる"), GUILayout.Width(80), GUILayout.Height(30)))
{
Close();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
GUILayout.Space(10);
}
// Localization helper
private string L(string en, string ja)
{
return currentLanguage == Language.Japanese ? ja : en;
}
private void DrawChangelogContent()
{
var sectionStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 14
};
var itemStyle = new GUIStyle(EditorStyles.label)
{
wordWrap = true,
richText = true
};
// v1.2.23
GUILayout.Label(L("v1.2.23 - run_csharp result capture + HTTP server stability", "v1.2.23 - run_csharp 戻り値捕捉 + HTTP サーバー安定化"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Fix (ESC-0107): run_csharp returned result:null for most snippets</b>", "<b>★ 修正 (ESC-0107): run_csharp が殆どの snippet で result:null を返していた問題</b>"), itemStyle);
GUILayout.Label(L("• Mono.CSharp.Evaluator.Run discards return values. We now detect the last top-level return keyword (depth-aware) and rewrite `return X;` into a static-field sink call, capturing X across multi-statement bodies / foreach{}/if{} blocks / multiplication expressions.", "• Mono.CSharp.Evaluator.Run は戻り値を破棄するため、最後の top-level return キーワードを深度考慮で検出し、`return X;` を静的フィールド経由の sink 呼び出しに書き換え。複文 / foreach { } / if { } / 乗算式すべてで X が捕捉されるようになった"), itemStyle);
GUILayout.Label(L("• Application.logMessageReceived hook added so Debug.Log / LogWarning / LogError lines appear in the `output` field (was missing — only Console.Out was captured)", "• Application.logMessageReceived フック追加。Debug.Log / LogWarning / LogError が output フィールドに反映 (従来は Console.Out のみで取得漏れ)"), itemStyle);
GUILayout.Label(L("• Known limitation: Mono parser cannot instantiate generic TYPES (new List&lt;int&gt;() etc). Workaround: use arrays / ArrayList / generic methods (FindObjectsByType&lt;T&gt;)", "• 既知制限: Mono パーサが generic 型インスタンス化 (new List&lt;int&gt;() 等) を解釈できない。回避策は配列 / ArrayList / generic メソッド (FindObjectsByType&lt;T&gt;) 利用"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix (ESC-0108): HTTP server WebSocket dropped every ~30s</b>", "<b>★ 修正 (ESC-0108): HTTP サーバー WebSocket が約30秒で切断</b>"), itemStyle);
GUILayout.Label(L("• Mono ClientWebSocket doesn't auto-pong protocol-level pings (unlike .NET 5+). Node side terminated the connection every heartbeat interval", "• Mono ClientWebSocket は .NET 5+ と異なり protocol-level ping に自動 pong しない。結果 Node 側が毎ハートビートで切断"), itemStyle);
GUILayout.Label(L("• http-server.js heartbeat now uses last-message-seen timestamps. Configurable via UNITY_STALE_TIMEOUT_MS env (default 60s)", "• http-server.js の heartbeat を last-message-seen 方式に置換。UNITY_STALE_TIMEOUT_MS 環境変数で調整可 (デフォルト 60s)"), itemStyle);
GUILayout.Label(L("• Reported and verified by xvpower. — thank you!", "• xvpower. さん報告・検証ありがとうございます"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: HTTP server died on macOS/Linux during domain reload</b>", "<b>★ 修正: macOS/Linux で domain reload のたびに HTTP サーバーが死亡</b>"), itemStyle);
GUILayout.Label(L("• Previous Process.Start with piped stdout/stderr caused node to hit SIGPIPE when Unity's C# domain reloaded the pipe readers", "• 旧 Process.Start は stdout/stderr を C# 側 pipe にリダイレクトしており、domain reload で pipe reader が消えて node が SIGPIPE で死亡"), itemStyle);
GUILayout.Label(L("• Replaced with `nohup node ... >log 2>&1 </dev/null &` detach. Process is independent of Unity's lifecycle, survives all recompiles", "• `nohup node ... >log 2>&1 </dev/null &` 経由の detach に変更。Unity ライフサイクルから独立、recompile で死なない"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: Auto Reconnect didn't engage on fresh installs</b>", "<b>★ 修正: 新規インストール時に Auto Reconnect が機能しなかった問題</b>"), itemStyle);
GUILayout.Label(L("• enableMCP default flipped from false → true. Unity is always a CLIENT of the MCP server (port 8090), the opt-in flag was a UX trap", "• enableMCP デフォルトを false → true に変更。Unity は常に MCP サーバー (port 8090) のクライアントなので opt-in 必要なフラグは UX トラップだった"), itemStyle);
GUILayout.Label(L("• Manual AI Reconnect, Auto Reconnect toggle, and successful connect all force enableMCP=true so it persists across domain reloads", "• 手動 AI Reconnect、Auto Reconnect トグル、接続成功時のいずれでも enableMCP=true を永続化、domain reload を跨いで維持"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Added: AI Connection tab connection-controls bar</b>", "<b>★ 追加: AI Connection タブに接続コントロールバー</b>"), itemStyle);
GUILayout.Label(L("• Live MCP status indicator + `AI Reconnect` button (silent) + `Auto Reconnect` checkbox + `Discord` shortcut. Surfaces Tools-menu items in the Setup window where users actually troubleshoot", "• MCP 接続ライブステータス + `AI Reconnect` ボタン (確認ダイアログなし) + `Auto Reconnect` チェックボックス + `Discord` ショートカット。Tools メニュー項目をユーザーが普段デバッグする Setup Window 内に集約"), itemStyle);
GUILayout.Label(L("• MCP Server: Start/Stop kept in Tools menu (advanced)", "• MCP Server: Start/Stop は引き続き Tools メニュー (上級向け)"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: port-mapping JSON corruption infinite log loop</b>", "<b>★ 修正: port-mapping JSON 破損による Console ログ無限ループ</b>"), itemStyle);
GUILayout.Label(L("• NexusProjectPortManager.LoadMapping recovery now deletes .backup before File.Move, then writes a fresh empty mapping. Previously the silent catch left the corrupt file in place, causing frame-rate Console spam after any concurrent write race", "• NexusProjectPortManager.LoadMapping の復旧処理を強化。.backup を事前削除し、復旧後すぐ空の有効 JSON を書き出す。従来は silent catch で破損ファイルが残存し書き込み競合後に毎フレーム Console エラーが出続けていた"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.22
GUILayout.Label(L("v1.2.22 - EMERGENCY HOTFIX: MCP timeout (ESC-0102)", "v1.2.22 - 緊急修正: MCP timeout 問題 (ESC-0102)"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Critical fix: MCP timeout after v1.2.20/21 update</b>", "<b>★ 重要修正: v1.2.20/21 アップデート後の MCP タイムアウト</b>"), itemStyle);
GUILayout.Label(L("• SynLog.Info called EditorPrefs.GetBool on every log — main-thread only, threw silently on background WebSocket Receive thread and killed the listener Task", "• SynLog.Info が毎回 EditorPrefs.GetBool を呼び、これがメインスレッド限定だったため WebSocket 受信スレッドで例外を投げ、リスナータスクが silent kill されていた"), itemStyle);
GUILayout.Label(L("• Fix: SynLog now caches verbose flag in a volatile bool, initialized via [InitializeOnLoadMethod]", "• 修正: SynLog の verbose flag を volatile bool でキャッシュ、[InitializeOnLoadMethod] で初期化"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: NexusEditorMCPService reconnect storm</b>", "<b>★ 修正: NexusEditorMCPService 再接続ストーム</b>"), itemStyle);
GUILayout.Label(L("• lastConnectionCheckTime was written via Stopwatch-since-classload but compared against Time.realtimeSinceStartup. After domain reload Stopwatch reset to 0 → reconnect gate always true → reconnects every frame", "• lastConnectionCheckTime が Stopwatch ベースで書き込まれ、Time.realtimeSinceStartup で読まれていた。ドメインリロード後に Stopwatch=0 になり差分が常に大きく、毎フレーム再接続ループ発生"), itemStyle);
GUILayout.Label(L("• Fix: ThreadSafeTime() now calibrates against Time.realtimeSinceStartup on first main-thread tick", "• 修正: ThreadSafeTime() を初回 main-thread tick で Time.realtimeSinceStartup と同期キャリブレーション"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ New: unity_run_csharp tool (equivalent of Blender's run_python)</b>", "<b>★ 新規: unity_run_csharp ツール (Blender run_python 相当)</b>"), itemStyle);
GUILayout.Label(L("• Execute arbitrary C# against the running Editor — UnityEngine / UnityEditor / Linq / Newtonsoft.Json pre-imported", "• 任意 C# を Editor 内で実行可能。UnityEngine / UnityEditor / Linq / Newtonsoft.Json プリインポート済み"), itemStyle);
GUILayout.Label(L("• Uses Mono.CSharp.Evaluator instance API + AppDomain assembly injection, does NOT trigger AssemblyReload", "• Mono.CSharp.Evaluator のインスタンス API + AppDomain アセンブリ注入。AssemblyReload を起こさない"), itemStyle);
GUILayout.Label(L("• Promoted to a SuperSave top-level meta-tool for direct invocation", "• SuperSave のトップレベル meta-tool に昇格、直接呼び出し可"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Diagnostics</b>", "<b>★ 診断改善</b>"), itemStyle);
GUILayout.Label(L("• index-supersave.js: ws error handlers, send callback, connection diagnostics", "• index-supersave.js: ws エラーハンドラ、send コールバック、接続診断ログ追加"), itemStyle);
GUILayout.Label(L("• NexusWebSocketClient.ReceiveLoop (HTTP path): fixed missing EndOfMessage concatenation that truncated >4KB messages", "• NexusWebSocketClient.ReceiveLoop (HTTP 経路): 4KB 超メッセージが切れる EndOfMessage 連結欠落バグ修正"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.21
GUILayout.Label(L("v1.2.21 - Windows HTTP Server Cascade Kill Fix (ESC-0095)", "v1.2.21 - Windows HTTP サーバー連鎖死問題の修正 (ESC-0095)"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Root cause finally identified</b>", "<b>★ 長年未解決だった根本原因を特定</b>"), itemStyle);
GUILayout.Label(L("• Unity Editor on Windows assigns itself a Win32 Job Object with KILL_ON_JOB_CLOSE", "• Unity Editor (Windows) は自身を Win32 Job Object に登録し KILL_ON_JOB_CLOSE フラグで管理している"), itemStyle);
GUILayout.Label(L("• Process.Start children inherit the Job and die on assembly reload / PlayMode transitions", "• Process.Start で起動した子プロセスはこの Job を継承し、アセンブリリロード / PlayMode 遷移で殺される"), itemStyle);
GUILayout.Label(L("• This is why the v1.2.10 → v1.2.11 internal→external rewrite did not fix it (node.exe was still in Unity's Job)", "• v1.2.10 → v1.2.11 で内部 C# → 外部 Node.js に切り出しても改善しなかったのはこのため (node.exe も Unity の Job 内にいた)"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: CreateProcessW with CREATE_BREAKAWAY_FROM_JOB</b>", "<b>★ 修正: CreateProcessW + CREATE_BREAKAWAY_FROM_JOB</b>"), itemStyle);
GUILayout.Label(L("• On Windows, node.exe is now spawned via P/Invoke CreateProcessW with BREAKAWAY_FROM_JOB | DETACHED_PROCESS | NEW_PROCESS_GROUP", "• Windows では P/Invoke で CreateProcessW を直接呼び、BREAKAWAY_FROM_JOB | DETACHED_PROCESS | NEW_PROCESS_GROUP を立てて spawn"), itemStyle);
GUILayout.Label(L("• The spawned node.exe now runs fully independent of Unity's Job Object", "• 起動した node.exe は Unity の Job から完全に独立"), itemStyle);
GUILayout.Label(L("• Same technique used by Unity's own Burst Compiler (BclApp.cs)", "• Unity 公式の Burst Compiler (BclApp.cs) と同じ技法"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: PID Recovery After Domain Reload</b>", "<b>★ 修正: ドメインリロード後の PID 復元</b>"), itemStyle);
GUILayout.Label(L("• Node PID stored in SessionState + EditorPrefs", "• Node PID を SessionState + EditorPrefs に保存"), itemStyle);
GUILayout.Label(L("• [InitializeOnLoadMethod] re-attaches by PID after reload and reconnects WebSocket only", "• [InitializeOnLoadMethod] でリロード後に PID から再接続。WebSocket だけ繋ぎ直し"), itemStyle);
GUILayout.Label(L("• No more 'Connect Unity Only' button required after script edits", "• スクリプト編集後に 'Connect Unity Only' を押す必要が無くなる"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: Parent-PID Watchdog (orphan guard)</b>", "<b>★ 修正: 親 PID watchdog (孤児防止)</b>"), itemStyle);
GUILayout.Label(L("• http-server.js now self-terminates 5s after Unity dies", "• http-server.js は Unity 死亡後 5秒以内に self-exit"), itemStyle);
GUILayout.Label(L("• Prevents zombie node.exe even when BREAKAWAY succeeds", "• BREAKAWAY 成功時でも node.exe ゾンビ化を防止"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Change: Detached log file</b>", "<b>★ 変更: detached モードのログファイル化</b>"), itemStyle);
GUILayout.Label(L("• DETACHED_PROCESS breaks stdout pipes, so node now writes to MCPServer/logs/http-server.log", "• DETACHED_PROCESS では stdout パイプが使えないため node 側でログファイル直書き (MCPServer/logs/http-server.log)"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ macOS / Linux behaviour unchanged</b>", "<b>★ macOS / Linux は従来通り</b>"), itemStyle);
GUILayout.Label(L("• No Job-Object-equivalent cascade-kill on these platforms — legacy Process.Start path retained", "• Job Object 相当の仕組みが無いため、従来の Process.Start 経路を維持"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.20
GUILayout.Label(L("v1.2.20 - Async Crash Fix & Log Cleanup", "v1.2.20 - 非同期クラッシュ修正 & ログ整理"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Fix: Async Thread Crash on Disconnect (ESC-0025)</b>", "<b>★ 修正: 切断時の非同期スレッドクラッシュ (ESC-0025)</b>"), itemStyle);
GUILayout.Label(L("• OnConnectionLost no longer crashes when called from non-main threads", "• OnConnectionLost がメインスレッド外から呼ばれてもクラッシュしない"), itemStyle);
GUILayout.Label(L("• Introduced ThreadSafeTime() using Stopwatch for async-safe timing", "• Stopwatch ベースの ThreadSafeTime() を導入し非同期安全な時刻取得に"), itemStyle);
GUILayout.Label(L("• Tool execution self-recovers without Unity restart", "• Unity を再起動せずにツール実行が自己復旧"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Change: Verbose Log Toggle</b>", "<b>★ 変更: 詳細ログのトグル</b>"), itemStyle);
GUILayout.Label(L("• Setup Window > HTTP Server > 'Verbose Logs' toggle", "• Setup Window > HTTP Server > 'Verbose Logs' で切り替え"), itemStyle);
GUILayout.Label(L("• Errors are always logged regardless of toggle", "• Error は常に表示される"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Change: Smaller Setup Window Min Size</b>", "<b>★ 変更: Setup Window の最小サイズ縮小</b>"), itemStyle);
GUILayout.Label(L("• 800x800 → 480x480, fits on small laptops and dockable", "• 800x800 → 480x480、小型ラップトップやドッキングに対応"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Safeguard: Auto-Update Validation</b>", "<b>★ セーフガード: 自動アップデート検証</b>"), itemStyle);
GUILayout.Label(L("• File size and marker checks prevent partial-archive overwrites", "• ファイルサイズとマーカー検証で破損アーカイブによる上書きを防止"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: Main Window Repaint Recursion</b>", "<b>★ 修正: メインウィンドウ再描画の無限再帰</b>"), itemStyle);
GUILayout.Label(L("• ThrottledRepaint() no longer recurses into itself", "• ThrottledRepaint() が自分自身を呼び続ける問題を修正"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.19
GUILayout.Label(L("v1.2.19 - Windows Stability (Community Contribution)", "v1.2.19 - Windows安定性 (コミュニティ貢献)"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Fix: Windows HTTP WebSocket Stability</b>", "<b>★ 修正: Windows HTTP WebSocket安定性</b>"), itemStyle);
GUILayout.Label(L("• Fixed HTTP server tab becoming unresponsive on Windows", "• WindowsでHTTPサーバータブが無反応になる問題を修正"), itemStyle);
GUILayout.Label(L("• Added reentrancy guard to prevent concurrent WebSocket connections", "• 同時WebSocket接続を防ぐ再入ガードを追加"), itemStyle);
GUILayout.Label(L("• Connect timeout (5s) prevents indefinite hang", "• 接続タイムアウト(5秒)で無限ハングを防止"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: MCP Reconnect Storm Prevention</b>", "<b>★ 修正: MCPリコネクト嵐の防止</b>"), itemStyle);
GUILayout.Label(L("• Added 10-second minimum interval between reconnect attempts", "• リコネクト試行間に最低10秒のインターバル追加"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ Fix: Setup Window UI Freeze</b>", "<b>★ 修正: Setup Window UIフリーズ</b>"), itemStyle);
GUILayout.Label(L("• Port check moved to background thread", "• ポートチェックをバックグラウンドスレッドに移動"), itemStyle);
GUILayout.Label(L("• Main Window repaint throttled to 10Hz", "• メインウィンドウの再描画を10Hzに制限"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ New: MCP Server Start/Stop Menu</b>", "<b>★ 新機能: MCP Server Start/Stopメニュー</b>"), itemStyle);
GUILayout.Label(L("• Tools > Synaptic Pro > MCP Server: Start/Stop", "• Tools > Synaptic Pro > MCP Server: Start/Stop"), itemStyle);
GUILayout.Space(5);
var creditStyle = new GUIStyle(itemStyle) { fontStyle = FontStyle.Italic };
GUILayout.Label(L("Special thanks to OverlordMethuselah777 for contributing these fixes!", "OverlordMethuselah777氏の修正貢献に感謝します!"), creditStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.18
GUILayout.Label(L("v1.2.18 - Auto-Update & WebSocket Stability", "v1.2.18 - 自動アップデート・WebSocket安定性"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ New: Auto-Update System</b>", "<b>★ 新機能: 自動アップデート</b>"), itemStyle);
GUILayout.Label(L("• Update check on Unity startup (once per day)", "• Unity起動時にアップデート確認(1日1回)"), itemStyle);
GUILayout.Label(L("• One-click update from Setup Window banner", "• Setup Windowのバナーからワンクリック更新"), itemStyle);
GUILayout.Label(L("<b>★ Fix: WebSocket Connection Stability</b>", "<b>★ 修正: WebSocket接続安定性</b>"), itemStyle);
GUILayout.Label(L("• Added ping/pong heartbeat every 30 seconds", "• 30秒ごとのping/pongハートビート追加"), itemStyle);
GUILayout.Label(L("• Reconnect attempts increased to 30 (2s intervals)", "• 再接続試行を30回に増加(2秒間隔)"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.17
GUILayout.Label(L("v1.2.17 - Editor Performance Fix", "v1.2.17 - エディタパフォーマンス修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Fix: HTTP Server Tab Performance</b>", "<b>★ 修正: HTTP Serverタブのパフォーマンス</b>"), itemStyle);
GUILayout.Label(L("• Fixed editor slowdown when HTTP Server tab is open but server not started", "• HTTPサーバー未起動時にHTTP Serverタブを開くとエディタが重くなる問題を修正"), itemStyle);
GUILayout.Label(L("• Server status check now runs every 5 seconds instead of every frame", "• サーバーステータス確認を毎フレームから5秒間隔に変更"), itemStyle);
GUILayout.Label(L("• Added UTF-8 encoding for Node.js process output", "• Node.jsプロセス出力のUTF-8エンコーディングを追加"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.16
GUILayout.Label(L("v1.2.16 - Unity 6 GUILayout Fix", "v1.2.16 - Unity 6 GUILayout修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Fix: Unity 6 GUILayout & Auto-Start</b>", "<b>★ 修正: Unity 6 GUILayout・Auto-Start</b>"), itemStyle);
GUILayout.Label(L("• Fixed GUILayout Begin/End mismatch error in DrawHeader and HTTP Server UI", "• DrawHeaderとHTTP Server UIのGUILayout Begin/End不一致エラーを修正"), itemStyle);
GUILayout.Label(L("• Fixed Auto-Start not working after domain reload", "• ドメインリロード後にAuto-Startが動作しない問題を修正"), itemStyle);
GUILayout.Label(L("• Auto-Start now detects existing server and reconnects instead of restarting", "• Auto-Start時に既存サーバーを検出し再接続するように改善"), itemStyle);
GUILayout.Label(L("• Added UTF-8 encoding for Node.js process output", "• Node.jsプロセス出力のUTF-8エンコーディングを追加"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.15
GUILayout.Label(L("v1.2.15 - Setup Window Performance Fix", "v1.2.15 - Setup Windowパフォーマンス修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Fix: Setup Window Freeze on Large Projects</b>", "<b>★ 修正: 大規模プロジェクトでのSetup Windowフリーズ</b>"), itemStyle);
GUILayout.Label(L("• FindMCPServerPath now uses cached result instead of searching every call", "• FindMCPServerPathの結果をキャッシュし毎回の検索を排除"), itemStyle);
GUILayout.Label(L("• Eliminated recursive AllDirectories search that caused 'Hold on' dialog on large projects", "• 大規模プロジェクトで「Hold on」を引き起こしていた再帰的全ディレクトリ検索を廃止"), itemStyle);
GUILayout.Label(L("• PackageCache search limited to com.synaptic* packages only", "• PackageCache検索をcom.synaptic*パッケージのみに限定"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.14
GUILayout.Label(L("v1.2.14 - Windows Stability & Process Management Fix", "v1.2.14 - Windows安定性・プロセス管理修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Fix: Hold on Dialog & Process Cleanup</b>", "<b>★ 修正: Hold onダイアログ・プロセス管理</b>"), itemStyle);
GUILayout.Label(L("• Fixed 'Hold on' dialog caused by blocking CheckSetupStatus on main thread", "• メインスレッドをブロックするCheckSetupStatusによる「Hold on」を修正"), itemStyle);
GUILayout.Label(L("• Improved Windows command path detection (git/node/npm) - matching MCP config robustness", "• Windowsでのコマンドパス検出を強化(git/node/npm- MCP設定と同等の堅牢性"), itemStyle);
GUILayout.Label(L("• Node.js process now properly killed on Unity quit and domain reload", "• Unity終了時・ドメインリロード時にNode.jsプロセスを確実に停止"), itemStyle);
GUILayout.Label(L("• Auto-Start now kills stale processes before launching", "• Auto-Start時に残存プロセスを終了してから起動"), itemStyle);
GUILayout.Label(L("• Fixed MCP port conflict when multiple Claude Code sessions connect", "• 複数Claude Codeセッション接続時のMCPポート競合を修正"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.13
GUILayout.Label(L("v1.2.13 - Setup Window Stability Fix", "v1.2.13 - Setup Window安定性修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Fix: Setup Window Repaint Loop</b>", "<b>★ 修正: Setup Windowの再描画ループ</b>"), itemStyle);
GUILayout.Label(L("• Fixed 'Hold on' dialog appearing endlessly when switching tabs during MCP connection", "• MCP接続中にタブを切り替えると「Hold on」ダイアログが無限に出る問題を修正"), itemStyle);
GUILayout.Label(L("• Repaint animation now only runs on AI Connection tab", "• 再描画アニメーションをAI Connectionタブのみに限定"), itemStyle);
GUILayout.Label(L("• Reduced console debug log output", "• コンソールのデバッグログ出力を削減"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.12
GUILayout.Label(L("v1.2.12 - Windows HTTP Server Fix", "v1.2.12 - Windows HTTPサーバー修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Fix: Windows HTTP Server Launch</b>", "<b>★ 修正: Windows HTTPサーバー起動</b>"), itemStyle);
GUILayout.Label(L("• Fixed HTTP Server failing to start on Windows when project path contains spaces", "• プロジェクトパスにスペースが含まれる場合にHTTPサーバーが起動しない問題を修正"), itemStyle);
GUILayout.Label(L("• Windows now launches Node.js via cmd.exe for reliable path handling", "• Windowsではcmd.exe経由でNode.jsを起動し、パス処理の信頼性を向上"), itemStyle);
GUILayout.Label(L("• Improved WebSocket connection retry logic (3 attempts with 2s intervals)", "• WebSocket接続のリトライロジックを改善(2秒間隔で3回リトライ)"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.11
GUILayout.Label(L("v1.2.11 - Claude Desktop CWD Fix", "v1.2.11 - Claude Desktop CWD修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Critical Fix: Claude Desktop MCP Configuration</b>", "<b>★ 重要修正: Claude Desktop MCP設定</b>"), itemStyle);
GUILayout.Label(L("• Added missing 'cwd' to Claude Desktop config (was only set for VS Code/Gemini/Cursor)", "• Claude Desktop設定に欠けていた'cwd'を追加(VS Code/Gemini/Cursor用には設定済みだった)"), itemStyle);
GUILayout.Label(L("• Fixes connection timeout on non-standard paths (spaces, Japanese, external drives)", "• 非標準パス(スペース、日本語、外部ドライブ)での接続タイムアウトを修正"), itemStyle);
GUILayout.Label(L("• Node.js now runs from MCPServer directory for reliable require() resolution", "• Node.jsがMCPServerディレクトリから実行され、require()の解決が確実に"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.10
GUILayout.Label(L("v1.2.10 - MCP Process Fix & Resources", "v1.2.10 - MCPプロセス修正 & リソース"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Critical Fix: MCP Dual-Process Startup Bug</b>", "<b>★ 重要修正: MCP二重プロセス起動バグ</b>"), itemStyle);
GUILayout.Label(L("• Fixed Claude Code starting 2 MCP processes simultaneously", "• Claude Codeが2つのMCPプロセスを同時起動する問題を修正"), itemStyle);
GUILayout.Label(L("• Added retry logic with WebSocket shutdown handover", "• WebSocketシャットダウンハンドオーバー付きリトライロジック追加"), itemStyle);
GUILayout.Label(L("• Graceful process takeover for reliable connection", "• 確実な接続のためのグレースフルプロセステイクオーバー"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ New: MCP Resources Protocol</b>", "<b>★ 新機能: MCPリソースプロトコル</b>"), itemStyle);
GUILayout.Label(L("• synaptic://tools/reference - Compact tools reference", "• synaptic://tools/reference - コンパクトツールリファレンス"), itemStyle);
GUILayout.Label(L("• synaptic://tools/reference/full - Full with inputSchema", "• synaptic://tools/reference/full - inputSchema付きフル版"), itemStyle);
GUILayout.Label(L("• GET /resources, /resources/read HTTP endpoints", "• GET /resources, /resources/read HTTPエンドポイント"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.9
GUILayout.Label(L("v1.2.9 - TcpListener HTTP Server", "v1.2.9 - TcpListener HTTPサーバー"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Critical Fix: Windows Port Reuse (Domain Reload)</b>", "<b>★ 重要修正: Windowsポート再利用 (ドメインリロード)</b>"), itemStyle);
GUILayout.Label(L("• Replaced HttpListener with TcpListener", "• HttpListenerをTcpListenerに置換"), itemStyle);
GUILayout.Label(L("• Added SO_REUSEADDR socket option", "• SO_REUSEADDRソケットオプション追加"), itemStyle);
GUILayout.Label(L("• Bypasses HTTP.sys kernel driver on Windows", "• WindowsでHTTP.sysカーネルドライバをバイパス"), itemStyle);
GUILayout.Label(L("• Reliable port recovery after script recompilation", "• スクリプトリコンパイル後のポート復帰が確実に"), itemStyle);
GUILayout.Label(L("• Removed dangerous ForceKillPortProcess code", "• 危険なForceKillPortProcessコードを削除"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.8
GUILayout.Label(L("v1.2.8 - Tool Search & Material Fix", "v1.2.8 - ツール検索 & マテリアル修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ New: Tool Search Endpoint</b>", "<b>★ 新機能: ツール検索エンドポイント</b>"), itemStyle);
GUILayout.Label(L("• GET /tools/search?q=keyword - Search by name, title, description", "• GET /tools/search?q=keyword - 名前・タイトル・説明で検索"), itemStyle);
GUILayout.Label(L("• Optional category filter and result limit", "• カテゴリフィルタと結果数制限をサポート"), itemStyle);
GUILayout.Label(L("• Relevance scoring for better results", "• 関連性スコアリングで最適な結果を表示"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ New: MCP search_tools Meta-Tool</b>", "<b>★ 新機能: MCP search_tools メタツール</b>"), itemStyle);
GUILayout.Label(L("• Search tools without knowing exact category", "• カテゴリ不明でもキーワード検索可能"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>Improved: HTTP Server Port Recovery</b>", "<b>改善: HTTPサーバーポート復帰</b>"), itemStyle);
GUILayout.Label(L("• Force kill process blocking port on startup", "• 起動時にポートをブロックするプロセスを強制終了"), itemStyle);
GUILayout.Label(L("• No more 'port already in use' errors", "• 「ポート使用中」エラーが発生しなくなりました"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>Fix: MeshRenderer Material Assignment</b>", "<b>修正: MeshRendererマテリアル設定</b>"), itemStyle);
GUILayout.Label(L("• Fixed circular reference error (Color.linear)", "• 循環参照エラー(Color.linear)を修正"), itemStyle);
GUILayout.Label(L("• Material/Texture now serialize correctly", "• Material/Textureが正常にシリアライズ"), itemStyle);
GUILayout.Space(5);
GUILayout.Label(L("<b>★ New: Console Log Filtering</b>", "<b>★ 新機能: コンソールログフィルタリング</b>"), itemStyle);
GUILayout.Label(L("• excludeSynaptic: Auto-filter internal logs (default: true)", "• excludeSynaptic: 内部ログ自動除外 (デフォルト: true)"), itemStyle);
GUILayout.Label(L("• filter/exclude: Custom include/exclude patterns", "• filter/exclude: カスタムパターンで絞り込み"), itemStyle);
GUILayout.Label(L("• groupByMessage: Deduplicate with count", "• groupByMessage: 重複ログを件数表示でまとめる"), itemStyle);
GUILayout.Label(L("• Reduces token usage significantly", "• トークン消費を大幅削減"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.7
GUILayout.Label(L("v1.2.7 - Windows Domain Reload Fix", "v1.2.7 - Windowsドメインリロード修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ Critical Fix: Domain Reload Recovery (Windows)</b>", "<b>★ 重要修正: ドメインリロード復帰 (Windows)</b>"), itemStyle);
GUILayout.Label(L("• Fixed HTTP server not recovering after script recompilation", "• スクリプトリコンパイル後にHTTPサーバーが復帰しない問題を修正"), itemStyle);
GUILayout.Label(L("• Added ForceReleasePort() before assembly reload", "• アセンブリリロード前にForceReleasePort()を追加"), itemStyle);
GUILayout.Label(L("• GC.Collect() ensures port is properly released", "• GC.Collect()でポートを確実に解放"), itemStyle);
GUILayout.Label(L("• Thread.Join(500) for graceful thread termination", "• Thread.Join(500)でスレッドを適切に終了"), itemStyle);
GUILayout.Label(L("• Increased retry attempts (15x) and delay (1s)", "• リトライ回数(15回)と間隔(1秒)を増加"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.6
GUILayout.Label(L("v1.2.6 - HTTP Server Stability", "v1.2.6 - HTTPサーバー安定性向上"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>Port Release & Binding Improvements</b>", "<b>ポート解放・バインディング改善</b>"), itemStyle);
GUILayout.Label(L("• Added Abort() on Stop for immediate port release", "• Stop時にAbort()追加で即座にポート解放"), itemStyle);
GUILayout.Label(L("• Added retry logic for TIME_WAIT state (Windows)", "• TIME_WAIT状態へのリトライ処理追加(Windows)"), itemStyle);
GUILayout.Label(L("• Up to 5 retries with 500ms delay", "• 最大5回リトライ、500ms間隔"), itemStyle);
GUILayout.Label(L("• Prevents 'port already in use' errors", "• 「ポートが使用中」エラーを防止"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.5
GUILayout.Label(L("v1.2.5 - VFX Graph Fixes & HTTP Prompt API", "v1.2.5 - VFX Graph修正 & HTTPプロンプトAPI"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ New: HTTP Prompt Endpoint</b>", "<b>★ 新機能: HTTPプロンプトエンドポイント</b>"), itemStyle);
GUILayout.Label(L("• GET /prompt - Fetch AI control prompt directly", "• GET /prompt - AIプロンプトを直接取得可能"), itemStyle);
GUILayout.Space(10);
GUILayout.Label(L("<b>★ New: Test Runner Auto-Execution</b>", "<b>★ 新機能: テストランナー自動実行</b>"), itemStyle);
GUILayout.Label(L("• unity_run_tests now executes tests automatically", "• unity_run_testsがテストを自動実行"), itemStyle);
GUILayout.Label(L("• operation: run, results, list", "• operation: run, results, list"), itemStyle);
GUILayout.Space(10);
GUILayout.Label(L("<b>VFX Graph Fixes</b>", "<b>VFX Graph修正</b>"), itemStyle);
GUILayout.Label(L("• Fixed add_context, add_parameter, add_block", "• add_context, add_parameter, add_blockを修正"), itemStyle);
GUILayout.Label(L("• Fixed set_attribute 'Ambiguous match' error", "• set_attributeの'Ambiguous match'エラー修正"), itemStyle);
GUILayout.Label(L("• Improved reflection handling for Unity 2022.3+", "• Unity 2022.3+のリフレクション処理改善"), itemStyle);
GUILayout.Space(10);
GUILayout.Label(L("<b>Other Fixes</b>", "<b>その他の修正</b>"), itemStyle);
GUILayout.Label(L("• HTTP server: Play mode transition stability", "• HTTPサーバー: Playモード切替時の安定性向上"), itemStyle);
GUILayout.Label(L("• Animator window now updates after script changes", "• スクリプト変更後にAnimatorウィンドウが更新"), itemStyle);
GUILayout.Label(L("• Reduced MCP connection log noise", "• MCP接続ログのノイズ削減"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.4
GUILayout.Label(L("v1.2.4 - Dynamic Meta-Tools & Critical Fix", "v1.2.4 - 動的メタツール & 重要修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ New: Dynamic Meta-Tools</b>", "<b>★ 新機能: 動的メタツール</b>"), itemStyle);
GUILayout.Label(L("• unity_dynamic_inspect - Inspect any component", "• unity_dynamic_inspect - 任意コンポーネントの検査"), itemStyle);
GUILayout.Label(L("• unity_dynamic_modify - Modify any property", "• unity_dynamic_modify - 任意プロパティの変更"), itemStyle);
GUILayout.Label(L("• unity_dynamic_create - Create objects/prefabs", "• unity_dynamic_create - オブジェクト/プレハブ作成"), itemStyle);
GUILayout.Label(L("• Available as inspect/modify/create in SuperSave", "• SuperSaveでinspect/modify/createとして利用可"), itemStyle);
GUILayout.Space(10);
GUILayout.Label(L("<b>Critical Fix</b>", "<b>重要な修正</b>"), itemStyle);
GUILayout.Label(L("• Fixed prefabs contaminating scene analysis", "• シーン分析にプレハブが混入する問題を修正"), itemStyle);
GUILayout.Label(L("• analyze_draw_calls now returns only scene objects", "• analyze_draw_callsがシーン内オブジェクトのみ返す"), itemStyle);
GUILayout.Space(10);
GUILayout.Label(L("<b>Other Fixes</b>", "<b>その他の修正</b>"), itemStyle);
GUILayout.Label(L("• HTTP server port cleanup on recompile", "• HTTPサーバーのポート解放問題修正"), itemStyle);
GUILayout.Label(L("• Script creation path parameter added", "• スクリプト作成のpath引数追加"), itemStyle);
GUILayout.Label(L("• Windows Node.js path detection improved", "• Windows Node.jsパス検出改善"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.3
GUILayout.Label(L("v1.2.3 - HTTP API Fix", "v1.2.3 - HTTP API修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>Fixed</b>", "<b>修正</b>"), itemStyle);
GUILayout.Label(L("• HTTP API: All tools now work via /execute, /batch", "• HTTP API: 全ツールが/execute, /batchで動作"), itemStyle);
GUILayout.Label(L("• Fixed 'Unknown operation' error for unmapped tools", "• マッピングなしツールの'Unknown operation'エラー修正"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.2
GUILayout.Label(L("v1.2.2 - SuperSave Fixes", "v1.2.2 - SuperSave修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>Fixed</b>", "<b>修正</b>"), itemStyle);
GUILayout.Label(L("• SuperSave execute tool now works correctly", "• SuperSaveのexecuteツールが正常に動作"), itemStyle);
GUILayout.Label(L("• All MCP clients use selected server mode", "• 全MCPクライアントで選択モードを使用"), itemStyle);
GUILayout.Label(L("• Component details for all types (not null)", "• 全コンポーネントの詳細情報を取得可能"), itemStyle);
GUILayout.Label(L("• Filter aliases: tag, layer, name accepted", "• フィルタ別名: tag, layer, name対応"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.1
GUILayout.Label(L("v1.2.1 - Hotfix", "v1.2.1 - 緊急修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>Fixed</b>", "<b>修正</b>"), itemStyle);
GUILayout.Label(L("• SuperSave Mode: Added shutdown handlers", "• SuperSave: シャットダウン処理追加"), itemStyle);
GUILayout.Label(L("• Proper MCP client cleanup on exit", "• MCP終了時の適切なクリーンアップ"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.2.0
GUILayout.Label(L("v1.2.0 - Token SuperSave Mode", "v1.2.0 - トークン SuperSave モード"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("<b>★ New: Token SuperSave Mode [Recommended]</b>", "<b>★ 新機能: Token SuperSave モード [推奨]</b>"), itemStyle);
GUILayout.Label(L("• 99% context reduction with only 3 meta-tools", "• 3つのメタツールで99%のコンテキスト削減"), itemStyle);
GUILayout.Label(L("• list_categories() - Discover tool categories", "• list_categories() - カテゴリ一覧"), itemStyle);
GUILayout.Label(L("• list_tools(category) - See tools & parameters", "• list_tools(category) - ツール詳細"), itemStyle);
GUILayout.Label(L("• execute(tool, params) - Run any of 350+ tools", "• execute(tool, params) - 350+ツール実行"), itemStyle);
GUILayout.Label(L("• Works with all MCP clients", "• 全MCPクライアント対応"), itemStyle);
GUILayout.Label(L("• Best for long AI sessions", "• 長いAIセッションに最適"), itemStyle);
GUILayout.Space(10);
GUILayout.Label(L("<b>Improvements</b>", "<b>改善</b>"), itemStyle);
GUILayout.Label(L("• Setup window redesigned with mode selection", "• セットアップ画面をモード選択式に刷新"), itemStyle);
GUILayout.Label(L("• SuperSave Mode set as default", "• SuperSaveモードをデフォルトに"), itemStyle);
GUILayout.Label(L("• Better error messages with suggestions", "• エラーメッセージに提案を追加"), itemStyle);
GUILayout.Label(L("• Tool registry loaded from JSON dynamically", "• ツール定義をJSONから動的読み込み"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.1.9
GUILayout.Label(L("v1.1.9 - Stability Fixes", "v1.1.9 - 安定性修正"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("• Batch tool format conversion fix", "• バッチツールのフォーマット変換修正"), itemStyle);
GUILayout.Label(L("• MCP stdio protocol stability (JSON-RPC)", "• MCP stdio プロトコル安定化"), itemStyle);
GUILayout.Label(L("• HTTP server localhost binding fix", "• HTTPサーバーのlocalhost接続修正"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// v1.1.8
GUILayout.Label(L("v1.1.8 - Sphere Skybox", "v1.1.8 - 球体スカイボックス"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label(L("• Sphere Skybox: Create skybox from any photo", "• 球体スカイボックス: 写真からスカイボックス生成"), itemStyle);
GUILayout.Label(L("• Multi-pipeline shader support", "• マルチパイプラインシェーダー対応"), itemStyle);
GUILayout.Label(L("• MCP server renamed to unity-synaptic", "• MCPサーバー名をunity-synapticに変更"), itemStyle);
EditorGUILayout.EndVertical();
GUILayout.Space(15);
// Links
GUILayout.Label(L("Links", "リンク"), sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button(L("Documentation", "ドキュメント"), GUILayout.Height(25)))
{
Application.OpenURL("https://synaptic-ai.net/docs");
}
if (GUILayout.Button("Discord", GUILayout.Height(25)))
{
Application.OpenURL("https://discord.gg/MXwHCVWmPe");
}
if (GUILayout.Button(L("Website", "ウェブサイト"), GUILayout.Height(25)))
{
Application.OpenURL("https://synaptic-ai.net");
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// Reset the "don't show" preference (for testing)
/// </summary>
[MenuItem("Tools/Synaptic Pro/Reset Changelog Preference", false, 101)]
public static void ResetPreference()
{
EditorPrefs.DeleteKey(PREF_KEY_LAST_VERSION);
EditorPrefs.DeleteKey(PREF_KEY_DONT_SHOW);
SynLog.Info("[Synaptic] Changelog preference reset. Will show on next startup.");
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 58854a77bb2b1482c96c04eb6ab06187
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusChangelogWindow.cs
uploadId: 920982
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 7c8f3e5a9b2d4f1c8e6a3d5b7f9e1c4a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusCinemachineHelper.cs
uploadId: 920982
@@ -0,0 +1,371 @@
using System;
using System.Diagnostics;
using UnityEngine;
using SynapticAIPro;
using UnityEditor;
namespace SynapticPro
{
/// <summary>
/// Cleanup MCP server on Unity exit or play mode change
/// </summary>
[InitializeOnLoad]
public static class NexusCleanupHandler
{
static NexusCleanupHandler()
{
// [ProjectLazarus patch — REVERTED 2026-04-23]
// We tried adding `afterAssemblyReload += Connect()` here to skip the up-to-10s
// wait for NexusEditorMCPService.Update's poll. But it raced with
// NexusSetupWindow.OnEnable's existing auto-start Connect call, and the resulting
// double-Connect created an infinite WS connect→die→reconnect loop. Reverted.
// Original symptom (WS reconnect lag after heavy ops) is acceptable; Tools/synaptic-server.cmd
// is the manual escape hatch when needed.
// Event on Unity editor exit
EditorApplication.wantsToQuit += OnEditorQuitting;
// Event on play mode change
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
// On domain reload (after script compilation, etc.)
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
// Cleanup existing MCP server processes on startup
CleanupExistingProcesses();
}
private static bool OnEditorQuitting()
{
SynLog.Info("[Synaptic] Unity exit detected - Cleaning up MCP server");
// Ensure MCP server is stopped
var stopTask = NexusMCPSetupManager.Instance?.StopMCPServer();
if (stopTask != null)
{
// Wait synchronously for async operation (max 5 seconds)
if (stopTask.Wait(5000))
{
SynLog.Info("[Synaptic] MCP server stopped successfully");
}
else
{
SynLog.Warn("[Synaptic] MCP server stop timed out");
}
}
CleanupMCPServer();
return true; // Continue Unity exit
}
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.ExitingEditMode || state == PlayModeStateChange.ExitingPlayMode)
{
SynLog.Info($"[Synaptic] Play mode change detected ({state}) - Checking MCP server");
// Cleanup as needed on play mode change
// Usually maintain server, but stop if there are issues
}
}
private static void OnBeforeAssemblyReload()
{
SynLog.Info("[Synaptic] Before assembly reload - Saving MCP server state");
// Handle state saving before assembly reload if needed
}
/// <summary>
/// Cleanup existing MCP server processes on startup
/// </summary>
private static void CleanupExistingProcesses()
{
try
{
// In new architecture, cleanup on Unity startup is not needed
// Claude Desktop manages MCP server
SynLog.Info("[Synaptic] Unity is now a MCP client - skipping server cleanup");
}
catch (Exception e)
{
UnityEngine.Debug.LogError($"[Synaptic] Error in cleanup: {e.Message}");
}
}
/// <summary>
/// Check if port is in use
/// </summary>
private static bool CheckPortInUse(int port)
{
try
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "/usr/sbin/lsof",
Arguments = $"-i :{port}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return !string.IsNullOrEmpty(output);
}
catch
{
// If lsof fails, try another method
return false;
}
}
/// <summary>
/// Cleanup MCP server
/// </summary>
private static void CleanupMCPServer()
{
try
{
KillNodeProcesses();
SynLog.Info("[Synaptic] MCP server cleaned up");
}
catch (Exception e)
{
UnityEngine.Debug.LogError($"[Synaptic] MCP server cleanup error: {e.Message}");
}
}
/// <summary>
/// Terminate Node.js processes
/// </summary>
private static void KillNodeProcesses()
{
try
{
var killProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "/usr/bin/pkill",
Arguments = "-f \"node.*index.js\"",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
killProcess.Start();
killProcess.WaitForExit();
// Wait a bit for process to fully terminate
System.Threading.Thread.Sleep(500);
}
catch (Exception e)
{
SynLog.Warn($"[Synaptic] Warning during process termination: {e.Message}");
}
}
/// <summary>
/// Search for available port
/// </summary>
private static int FindAvailablePort(int startPort, int endPort)
{
for (int port = startPort; port <= endPort; port++)
{
if (!CheckPortInUse(port))
{
return port;
}
}
return -1; // Not found
}
/// <summary>
/// Update MCP client ports (HTTP + WebSocket)
/// </summary>
private static void UpdateMCPClientPorts(int httpPort, int wsPort)
{
try
{
#if UNITY_EDITOR
// Update WebSocket client port (Editor only)
try
{
var webSocketClientType = System.Type.GetType("NexusAIConnect.NexusWebSocketClient");
if (webSocketClientType != null)
{
var instanceProperty = webSocketClientType.GetProperty("Instance", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var instance = instanceProperty?.GetValue(null);
if (instance != null)
{
var setServerUrlMethod = webSocketClientType.GetMethod("SetServerUrl");
setServerUrlMethod?.Invoke(instance, new object[] { $"ws://localhost:{wsPort}" });
SynLog.Info($"[Synaptic] WebSocket client port updated to {wsPort}");
}
}
}
catch (System.Exception ex)
{
SynLog.Warn($"[Synaptic] Failed to update WebSocket client: {ex.Message}");
}
#endif
// Update Editor MCP Service port
try
{
var editorMCPServiceType = System.Type.GetType("NexusAIConnect.NexusEditorMCPService");
if (editorMCPServiceType != null)
{
var setServerUrlMethod = editorMCPServiceType.GetMethod("SetServerUrl", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
setServerUrlMethod?.Invoke(null, new object[] { $"ws://localhost:{wsPort}" });
SynLog.Info($"[Synaptic] Editor MCP Service port updated to {wsPort}");
}
}
catch (System.Exception ex)
{
SynLog.Warn($"[Synaptic] Failed to update Editor MCP Service: {ex.Message}");
}
// Update MCP client port (for Runtime)
var mcpClient = NexusMCPClient.Instance;
if (mcpClient != null)
{
mcpClient.SetServerUrl($"ws://localhost:{wsPort}");
SynLog.Info($"[Synaptic] MCP client port updated to {wsPort}");
}
// Update Claude Desktop configuration
if (wsPort != 8090 || httpPort != 3000)
{
UpdateClaudeDesktopConfig(httpPort, wsPort);
}
// HTTP port settings (for future expansion)
if (httpPort != 3000)
{
SynLog.Info($"[Synaptic] HTTP port changed to {httpPort} (will apply on MCP server startup)");
// TODO: Update MCP server config file or environment variables
System.Environment.SetEnvironmentVariable("NEXUS_HTTP_PORT", httpPort.ToString());
}
}
catch (System.Exception e)
{
UnityEngine.Debug.LogError($"[Synaptic] Error during port update: {e.Message}");
}
}
/// <summary>
/// Dynamically update Claude Desktop config file ports (HTTP + WebSocket)
/// </summary>
private static void UpdateClaudeDesktopConfig(int newHttpPort, int newWsPort)
{
try
{
// Detect platform-specific Claude Desktop config path
string configPath;
if (UnityEngine.Application.platform == UnityEngine.RuntimePlatform.OSXEditor)
{
configPath = System.IO.Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
"Library", "Application Support", "Claude", "claude_desktop_config.json"
);
}
else if (UnityEngine.Application.platform == UnityEngine.RuntimePlatform.WindowsEditor)
{
configPath = System.IO.Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
"Claude", "claude_desktop_config.json"
);
}
else
{
SynLog.Warn("[Synaptic] Unsupported platform for Claude Desktop config update");
return;
}
if (!System.IO.File.Exists(configPath))
{
SynLog.Warn($"[Synaptic] Claude Desktop config file not found: {configPath}");
return;
}
// Load config file
string configContent = System.IO.File.ReadAllText(configPath);
bool updated = false;
// Update WebSocket port (supports multiple patterns)
string newWsPattern = $"ws://localhost:{newWsPort}";
string[] oldWsPatterns = {
"ws://localhost:8090",
"ws://localhost:8081",
"ws://localhost:8082",
"ws://localhost:8083",
"ws://localhost:8084"
};
foreach (string oldWsPattern in oldWsPatterns)
{
if (configContent.Contains(oldWsPattern) && oldWsPattern != newWsPattern)
{
configContent = configContent.Replace(oldWsPattern, newWsPattern);
SynLog.Info($"[Synaptic] WebSocket port updated: {oldWsPattern} → {newWsPattern}");
updated = true;
}
}
// Update HTTP port
string oldHttpPattern = "http://localhost:3000";
string newHttpPattern = $"http://localhost:{newHttpPort}";
if (configContent.Contains(oldHttpPattern))
{
configContent = configContent.Replace(oldHttpPattern, newHttpPattern);
SynLog.Info($"[Synaptic] HTTP port updated: {oldHttpPattern} → {newHttpPattern}");
updated = true;
}
// Update port number only ("port": 3000 → "port": 3001)
string oldPortPattern = "\"port\": 3000";
string newPortPattern = $"\"port\": {newHttpPort}";
if (configContent.Contains(oldPortPattern))
{
configContent = configContent.Replace(oldPortPattern, newPortPattern);
SynLog.Info($"[Synaptic] Port setting updated: {oldPortPattern} → {newPortPattern}");
updated = true;
}
if (updated)
{
// Create backup
string backupPath = configPath + $".backup_{System.DateTime.Now:yyyyMMdd_HHmmss}";
System.IO.File.Copy(configPath, backupPath);
// Write new configuration
System.IO.File.WriteAllText(configPath, configContent);
SynLog.Info($"[Synaptic] Claude Desktop configuration updated");
SynLog.Info($"[Synaptic] Backup created: {backupPath}");
SynLog.Info($"[Synaptic] 🔄 Please restart Claude Desktop");
}
else
{
SynLog.Warn($"[Synaptic] No update targets found in config file");
}
}
catch (System.Exception e)
{
UnityEngine.Debug.LogError($"[Synaptic] Claude Desktop config update error: {e.Message}");
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 24f165b7a5bac4665ac1a31f7ed68729
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusCleanupHandler.cs
uploadId: 920982
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 2fc1cd5d4a34c4ba8a53030b8e71547d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusEditorMCPService.cs
uploadId: 920982
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 13df24e3cf3dc4e3493cdd3e012b4dc6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusExecutor.cs
uploadId: 920982
@@ -0,0 +1,989 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Newtonsoft.Json;
namespace SynapticPro
{
/// <summary>
/// Dynamic Meta-Tools for NexusUnityExecutor
/// Provides reflection-based inspection and modification of Unity components
/// </summary>
public partial class NexusUnityExecutor
{
#region Dynamic Meta-Tools
/// <summary>
/// Dynamically inspect any Unity object, component, scene, or project assets
/// </summary>
private string DynamicInspect(Dictionary<string, string> parameters)
{
try
{
var target = parameters.GetValueOrDefault("target", "gameobject").ToLower();
var name = parameters.GetValueOrDefault("name", "");
var componentType = parameters.GetValueOrDefault("component", "");
var path = parameters.GetValueOrDefault("path", "");
int.TryParse(parameters.GetValueOrDefault("depth", "2"), out int depth);
switch (target)
{
case "gameobject":
return InspectGameObject(name, depth);
case "component":
return InspectComponent(name, componentType, depth);
case "scene":
return InspectScene();
case "hierarchy":
return InspectHierarchy(depth);
case "prefabs":
return InspectPrefabs(path);
case "project":
return InspectProject(path);
default:
return JsonConvert.SerializeObject(new { error = $"Unknown inspect target: {target}" });
}
}
catch (Exception e)
{
return CreateErrorResponse("DynamicInspect", e, parameters);
}
}
private string InspectGameObject(string name, int depth)
{
if (string.IsNullOrEmpty(name))
{
// List all root GameObjects
var rootObjects = UnityEngine.SceneManagement.SceneManager.GetActiveScene()
.GetRootGameObjects()
.Select(go => new {
name = go.name,
active = go.activeSelf,
components = go.GetComponents<Component>().Select(c => c?.GetType().Name).Where(n => n != null).ToList(),
childCount = go.transform.childCount
}).ToList();
return JsonConvert.SerializeObject(new {
success = true,
message = $"Found {rootObjects.Count} root GameObjects",
gameObjects = rootObjects
});
}
var gameObject = GameObject.Find(name);
if (gameObject == null)
{
gameObject = FindGameObjectByPathDynamic(name);
}
if (gameObject == null)
{
return JsonConvert.SerializeObject(new { error = $"GameObject '{name}' not found" });
}
var components = new List<object>();
foreach (var comp in gameObject.GetComponents<Component>())
{
if (comp == null) continue;
components.Add(new {
type = comp.GetType().Name,
fullType = comp.GetType().FullName,
enabled = (comp is Behaviour b) ? b.enabled : true
});
}
var children = new List<object>();
if (depth > 0)
{
foreach (Transform child in gameObject.transform)
{
children.Add(new {
name = child.name,
active = child.gameObject.activeSelf,
componentCount = child.GetComponents<Component>().Length,
childCount = child.childCount
});
}
}
return JsonConvert.SerializeObject(new {
success = true,
gameObject = new {
name = gameObject.name,
active = gameObject.activeSelf,
layer = LayerMask.LayerToName(gameObject.layer),
tag = gameObject.tag,
isStatic = gameObject.isStatic,
transform = new {
position = new { x = gameObject.transform.position.x, y = gameObject.transform.position.y, z = gameObject.transform.position.z },
rotation = new { x = gameObject.transform.eulerAngles.x, y = gameObject.transform.eulerAngles.y, z = gameObject.transform.eulerAngles.z },
scale = new { x = gameObject.transform.localScale.x, y = gameObject.transform.localScale.y, z = gameObject.transform.localScale.z }
},
components = components,
children = children
}
});
}
private string InspectComponent(string gameObjectName, string componentType, int depth)
{
if (string.IsNullOrEmpty(gameObjectName))
{
return JsonConvert.SerializeObject(new { error = "GameObject name required" });
}
var gameObject = GameObject.Find(gameObjectName) ?? FindGameObjectByPathDynamic(gameObjectName);
if (gameObject == null)
{
return JsonConvert.SerializeObject(new { error = $"GameObject '{gameObjectName}' not found" });
}
Component component = null;
if (!string.IsNullOrEmpty(componentType))
{
component = gameObject.GetComponents<Component>()
.FirstOrDefault(c => c != null &&
(c.GetType().Name.Equals(componentType, StringComparison.OrdinalIgnoreCase) ||
c.GetType().Name.EndsWith(componentType, StringComparison.OrdinalIgnoreCase)));
}
if (component == null && !string.IsNullOrEmpty(componentType))
{
return JsonConvert.SerializeObject(new {
error = $"Component '{componentType}' not found on '{gameObjectName}'",
availableComponents = gameObject.GetComponents<Component>()
.Where(c => c != null)
.Select(c => c.GetType().Name)
.ToList()
});
}
// If no specific component, list all with their properties
if (component == null)
{
var allComponents = new List<object>();
foreach (var comp in gameObject.GetComponents<Component>())
{
if (comp == null) continue;
allComponents.Add(new {
type = comp.GetType().Name,
properties = GetSerializedPropertiesDynamic(comp, depth)
});
}
return JsonConvert.SerializeObject(new {
success = true,
gameObject = gameObjectName,
components = allComponents
});
}
// Inspect specific component
var properties = GetSerializedPropertiesDynamic(component, depth);
return JsonConvert.SerializeObject(new {
success = true,
gameObject = gameObjectName,
component = componentType,
type = component.GetType().FullName,
properties = properties
});
}
private List<object> GetSerializedPropertiesDynamic(Component component, int depth)
{
var properties = new List<object>();
var so = new SerializedObject(component);
var iterator = so.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
if (iterator.depth > depth)
{
enterChildren = false;
continue;
}
enterChildren = true;
var propInfo = new Dictionary<string, object>
{
["path"] = iterator.propertyPath,
["name"] = iterator.name,
["type"] = iterator.propertyType.ToString(),
["editable"] = iterator.editable
};
// Get value based on type
switch (iterator.propertyType)
{
case SerializedPropertyType.Integer:
propInfo["value"] = iterator.intValue;
break;
case SerializedPropertyType.Float:
propInfo["value"] = iterator.floatValue;
break;
case SerializedPropertyType.Boolean:
propInfo["value"] = iterator.boolValue;
break;
case SerializedPropertyType.String:
propInfo["value"] = iterator.stringValue;
break;
case SerializedPropertyType.Enum:
propInfo["value"] = iterator.enumDisplayNames?.Length > iterator.enumValueIndex && iterator.enumValueIndex >= 0
? iterator.enumDisplayNames[iterator.enumValueIndex]
: iterator.enumValueIndex.ToString();
propInfo["options"] = iterator.enumDisplayNames;
break;
case SerializedPropertyType.Vector2:
propInfo["value"] = new { x = iterator.vector2Value.x, y = iterator.vector2Value.y };
break;
case SerializedPropertyType.Vector3:
propInfo["value"] = new { x = iterator.vector3Value.x, y = iterator.vector3Value.y, z = iterator.vector3Value.z };
break;
case SerializedPropertyType.Vector4:
propInfo["value"] = new { x = iterator.vector4Value.x, y = iterator.vector4Value.y, z = iterator.vector4Value.z, w = iterator.vector4Value.w };
break;
case SerializedPropertyType.Color:
propInfo["value"] = new { r = iterator.colorValue.r, g = iterator.colorValue.g, b = iterator.colorValue.b, a = iterator.colorValue.a };
break;
case SerializedPropertyType.ObjectReference:
propInfo["value"] = iterator.objectReferenceValue?.name ?? "null";
propInfo["objectType"] = iterator.objectReferenceValue?.GetType().Name ?? "null";
break;
case SerializedPropertyType.LayerMask:
propInfo["value"] = iterator.intValue;
break;
case SerializedPropertyType.Rect:
var rect = iterator.rectValue;
propInfo["value"] = new { x = rect.x, y = rect.y, width = rect.width, height = rect.height };
break;
case SerializedPropertyType.ArraySize:
propInfo["value"] = iterator.intValue;
break;
case SerializedPropertyType.AnimationCurve:
propInfo["value"] = $"AnimationCurve with {iterator.animationCurveValue?.keys?.Length ?? 0} keys";
break;
default:
propInfo["value"] = "(complex type)";
break;
}
properties.Add(propInfo);
}
return properties;
}
private string InspectScene()
{
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
var rootObjects = scene.GetRootGameObjects();
int totalObjects = 0;
int totalComponents = 0;
var componentCounts = new Dictionary<string, int>();
void CountRecursive(GameObject go)
{
totalObjects++;
foreach (var comp in go.GetComponents<Component>())
{
if (comp == null) continue;
totalComponents++;
var typeName = comp.GetType().Name;
componentCounts[typeName] = componentCounts.GetValueOrDefault(typeName, 0) + 1;
}
foreach (Transform child in go.transform)
{
CountRecursive(child.gameObject);
}
}
foreach (var root in rootObjects)
{
CountRecursive(root);
}
return JsonConvert.SerializeObject(new {
success = true,
scene = new {
name = scene.name,
path = scene.path,
isDirty = scene.isDirty,
rootCount = rootObjects.Length,
totalGameObjects = totalObjects,
totalComponents = totalComponents,
topComponents = componentCounts
.OrderByDescending(x => x.Value)
.Take(20)
.Select(x => new { type = x.Key, count = x.Value })
.ToList()
}
});
}
private string InspectHierarchy(int depth)
{
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
var rootObjects = scene.GetRootGameObjects();
object BuildHierarchy(GameObject go, int currentDepth)
{
var children = new List<object>();
if (currentDepth < depth)
{
foreach (Transform child in go.transform)
{
children.Add(BuildHierarchy(child.gameObject, currentDepth + 1));
}
}
return new {
name = go.name,
active = go.activeSelf,
components = go.GetComponents<Component>()
.Where(c => c != null)
.Select(c => c.GetType().Name)
.ToList(),
children = children.Count > 0 ? children : null
};
}
var hierarchy = rootObjects.Select(go => BuildHierarchy(go, 0)).ToList();
return JsonConvert.SerializeObject(new {
success = true,
scene = scene.name,
depth = depth,
hierarchy = hierarchy
});
}
private string InspectPrefabs(string pathFilter)
{
try
{
var searchPath = string.IsNullOrEmpty(pathFilter) ? "Assets" : pathFilter.Replace("*", "");
if (searchPath.EndsWith("/")) searchPath = searchPath.TrimEnd('/');
if (!searchPath.StartsWith("Assets")) searchPath = "Assets";
var guids = AssetDatabase.FindAssets("t:Prefab", new[] { searchPath });
var prefabs = guids
.Select(guid => AssetDatabase.GUIDToAssetPath(guid))
.Where(p => string.IsNullOrEmpty(pathFilter) || MatchesWildcardDynamic(p, pathFilter))
.Take(100)
.Select(p => {
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(p);
return new {
path = p,
name = prefab?.name ?? Path.GetFileNameWithoutExtension(p),
components = prefab?.GetComponents<Component>()
.Where(c => c != null)
.Select(c => c.GetType().Name)
.ToList() ?? new List<string>()
};
})
.ToList();
return JsonConvert.SerializeObject(new {
success = true,
filter = pathFilter ?? "all",
count = prefabs.Count,
prefabs = prefabs
});
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new { error = e.Message });
}
}
private string InspectProject(string pathFilter)
{
try
{
var searchPath = string.IsNullOrEmpty(pathFilter) ? "Assets" : pathFilter;
if (!searchPath.StartsWith("Assets")) searchPath = "Assets/" + searchPath;
searchPath = searchPath.TrimEnd('/');
// Get folder structure
string[] folders = new string[0];
try
{
folders = AssetDatabase.GetSubFolders(searchPath);
}
catch { }
// Get assets in current folder
var guids = AssetDatabase.FindAssets("", new[] { searchPath });
var assets = guids
.Select(guid => AssetDatabase.GUIDToAssetPath(guid))
.Where(p => Path.GetDirectoryName(p).Replace("\\", "/") == searchPath)
.Take(50)
.Select(p => new {
path = p,
name = Path.GetFileName(p),
type = AssetDatabase.GetMainAssetTypeAtPath(p)?.Name ?? "Unknown"
})
.ToList();
return JsonConvert.SerializeObject(new {
success = true,
currentPath = searchPath,
folders = folders,
assets = assets
});
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new { error = e.Message });
}
}
private bool MatchesWildcardDynamic(string path, string pattern)
{
if (string.IsNullOrEmpty(pattern)) return true;
var regex = "^" + System.Text.RegularExpressions.Regex.Escape(pattern)
.Replace("\\*\\*", ".*")
.Replace("\\*", "[^/]*")
.Replace("\\?", ".") + "$";
return System.Text.RegularExpressions.Regex.IsMatch(path, regex, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
}
private GameObject FindGameObjectByPathDynamic(string path)
{
if (string.IsNullOrEmpty(path)) return null;
var parts = path.Split('/');
GameObject current = null;
foreach (var part in parts)
{
if (current == null)
{
current = GameObject.Find(part);
if (current == null)
{
var roots = UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects();
current = roots.FirstOrDefault(r => r.name == part);
}
}
else
{
var child = current.transform.Find(part);
current = child?.gameObject;
}
if (current == null) return null;
}
return current;
}
/// <summary>
/// Dynamically modify any property of a Unity component
/// </summary>
private string DynamicModify(Dictionary<string, string> parameters)
{
try
{
var gameObjectName = parameters.GetValueOrDefault("gameObject", "");
var componentType = parameters.GetValueOrDefault("component", "");
var propertiesJson = parameters.GetValueOrDefault("properties", "{}");
bool.TryParse(parameters.GetValueOrDefault("createIfMissing", "false"), out bool createIfMissing);
if (string.IsNullOrEmpty(gameObjectName))
{
return JsonConvert.SerializeObject(new { error = "GameObject name required" });
}
var gameObject = GameObject.Find(gameObjectName) ?? FindGameObjectByPathDynamic(gameObjectName);
if (gameObject == null)
{
return JsonConvert.SerializeObject(new { error = $"GameObject '{gameObjectName}' not found" });
}
// Find or create component
Component component = null;
if (!string.IsNullOrEmpty(componentType))
{
component = gameObject.GetComponents<Component>()
.FirstOrDefault(c => c != null &&
(c.GetType().Name.Equals(componentType, StringComparison.OrdinalIgnoreCase) ||
c.GetType().Name.EndsWith(componentType, StringComparison.OrdinalIgnoreCase)));
if (component == null && createIfMissing)
{
var type = FindComponentTypeDynamic(componentType);
if (type != null)
{
component = gameObject.AddComponent(type);
}
}
if (component == null)
{
return JsonConvert.SerializeObject(new {
error = $"Component '{componentType}' not found on '{gameObjectName}'",
hint = createIfMissing ? "Could not create component - type not found" : "Use createIfMissing:true to add it"
});
}
}
else
{
return JsonConvert.SerializeObject(new { error = "Component type required" });
}
// Parse properties
var properties = JsonConvert.DeserializeObject<Dictionary<string, object>>(propertiesJson);
if (properties == null || properties.Count == 0)
{
return JsonConvert.SerializeObject(new { error = "No properties specified" });
}
var so = new SerializedObject(component);
var modifiedProperties = new List<string>();
var failedProperties = new List<object>();
foreach (var kvp in properties)
{
var prop = so.FindProperty(kvp.Key);
if (prop == null)
{
failedProperties.Add(new { path = kvp.Key, error = "Property not found" });
continue;
}
if (!prop.editable)
{
failedProperties.Add(new { path = kvp.Key, error = "Property not editable" });
continue;
}
try
{
SetSerializedPropertyValueDynamic(prop, kvp.Value);
modifiedProperties.Add(kvp.Key);
}
catch (Exception e)
{
failedProperties.Add(new { path = kvp.Key, error = e.Message });
}
}
so.ApplyModifiedProperties();
EditorUtility.SetDirty(gameObject);
return JsonConvert.SerializeObject(new {
success = true,
gameObject = gameObjectName,
component = componentType,
modifiedProperties = modifiedProperties,
failedProperties = failedProperties.Count > 0 ? failedProperties : null
});
}
catch (Exception e)
{
return CreateErrorResponse("DynamicModify", e, parameters);
}
}
private void SetSerializedPropertyValueDynamic(SerializedProperty prop, object value)
{
switch (prop.propertyType)
{
case SerializedPropertyType.Integer:
prop.intValue = Convert.ToInt32(value);
break;
case SerializedPropertyType.Float:
prop.floatValue = Convert.ToSingle(value);
break;
case SerializedPropertyType.Boolean:
prop.boolValue = Convert.ToBoolean(value);
break;
case SerializedPropertyType.String:
prop.stringValue = value?.ToString() ?? "";
break;
case SerializedPropertyType.Enum:
if (value is int intVal)
prop.enumValueIndex = intVal;
else if (value is long longVal)
prop.enumValueIndex = (int)longVal;
else if (value is string strVal)
{
var idx = Array.IndexOf(prop.enumDisplayNames, strVal);
if (idx >= 0) prop.enumValueIndex = idx;
else if (int.TryParse(strVal, out int parsed)) prop.enumValueIndex = parsed;
}
break;
case SerializedPropertyType.Vector2:
if (value is Newtonsoft.Json.Linq.JObject v2)
prop.vector2Value = new Vector2(v2["x"]?.ToObject<float>() ?? 0, v2["y"]?.ToObject<float>() ?? 0);
break;
case SerializedPropertyType.Vector3:
if (value is Newtonsoft.Json.Linq.JObject v3)
prop.vector3Value = new Vector3(v3["x"]?.ToObject<float>() ?? 0, v3["y"]?.ToObject<float>() ?? 0, v3["z"]?.ToObject<float>() ?? 0);
break;
case SerializedPropertyType.Vector4:
if (value is Newtonsoft.Json.Linq.JObject v4)
prop.vector4Value = new Vector4(v4["x"]?.ToObject<float>() ?? 0, v4["y"]?.ToObject<float>() ?? 0, v4["z"]?.ToObject<float>() ?? 0, v4["w"]?.ToObject<float>() ?? 0);
break;
case SerializedPropertyType.Color:
if (value is Newtonsoft.Json.Linq.JObject c)
prop.colorValue = new Color(c["r"]?.ToObject<float>() ?? 1, c["g"]?.ToObject<float>() ?? 1, c["b"]?.ToObject<float>() ?? 1, c["a"]?.ToObject<float>() ?? 1);
else if (value is string hex)
prop.colorValue = ParseHexColorDynamic(hex);
break;
case SerializedPropertyType.LayerMask:
prop.intValue = Convert.ToInt32(value);
break;
default:
throw new NotSupportedException($"Property type {prop.propertyType} not supported for direct modification");
}
}
private Color ParseHexColorDynamic(string hex)
{
if (string.IsNullOrEmpty(hex)) return Color.white;
hex = hex.TrimStart('#');
if (hex.Length == 6)
{
byte r = Convert.ToByte(hex.Substring(0, 2), 16);
byte g = Convert.ToByte(hex.Substring(2, 2), 16);
byte b = Convert.ToByte(hex.Substring(4, 2), 16);
return new Color32(r, g, b, 255);
}
return Color.white;
}
private Type FindComponentTypeDynamic(string typeName)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
try
{
var type = assembly.GetTypes()
.FirstOrDefault(t => typeof(Component).IsAssignableFrom(t) &&
(t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase) ||
t.Name.EndsWith(typeName, StringComparison.OrdinalIgnoreCase)));
if (type != null) return type;
}
catch { }
}
return null;
}
/// <summary>
/// Universal creation tool for GameObjects, prefabs, scenes, and components
/// </summary>
private string DynamicCreate(Dictionary<string, string> parameters)
{
try
{
var createType = parameters.GetValueOrDefault("type", "gameobject").ToLower();
switch (createType)
{
case "gameobject":
return CreateDynamicGameObject(parameters);
case "prefab":
return CreateDynamicPrefabInstance(parameters);
case "scene":
return LoadDynamicScene(parameters);
case "component":
return AddDynamicComponent(parameters);
default:
return JsonConvert.SerializeObject(new { error = $"Unknown create type: {createType}" });
}
}
catch (Exception e)
{
return CreateErrorResponse("DynamicCreate", e, parameters);
}
}
private string CreateDynamicGameObject(Dictionary<string, string> parameters)
{
var name = parameters.GetValueOrDefault("name", "New GameObject");
var primitive = parameters.GetValueOrDefault("primitive", "empty").ToLower();
var parentName = parameters.GetValueOrDefault("parent", "");
GameObject go;
switch (primitive)
{
case "cube": go = GameObject.CreatePrimitive(PrimitiveType.Cube); break;
case "sphere": go = GameObject.CreatePrimitive(PrimitiveType.Sphere); break;
case "cylinder": go = GameObject.CreatePrimitive(PrimitiveType.Cylinder); break;
case "plane": go = GameObject.CreatePrimitive(PrimitiveType.Plane); break;
case "capsule": go = GameObject.CreatePrimitive(PrimitiveType.Capsule); break;
case "quad": go = GameObject.CreatePrimitive(PrimitiveType.Quad); break;
default: go = new GameObject(); break;
}
go.name = name;
if (!string.IsNullOrEmpty(parentName))
{
var parent = GameObject.Find(parentName) ?? FindGameObjectByPathDynamic(parentName);
if (parent != null)
{
go.transform.SetParent(parent.transform, false);
}
}
ApplyTransformFromParametersDynamic(go, parameters);
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
Selection.activeGameObject = go;
return JsonConvert.SerializeObject(new {
success = true,
message = $"Created GameObject '{name}'",
gameObject = new {
name = go.name,
primitive = primitive,
path = GetGameObjectPathDynamic(go)
}
});
}
private string CreateDynamicPrefabInstance(Dictionary<string, string> parameters)
{
var assetPath = parameters.GetValueOrDefault("asset", "");
var instanceName = parameters.GetValueOrDefault("name", "");
var parentName = parameters.GetValueOrDefault("parent", "");
if (string.IsNullOrEmpty(assetPath))
{
return JsonConvert.SerializeObject(new { error = "Asset path required" });
}
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
if (prefab == null)
{
var guids = AssetDatabase.FindAssets($"{Path.GetFileNameWithoutExtension(assetPath)} t:Prefab");
if (guids.Length > 0)
{
var foundPath = AssetDatabase.GUIDToAssetPath(guids[0]);
prefab = AssetDatabase.LoadAssetAtPath<GameObject>(foundPath);
}
}
if (prefab == null)
{
return JsonConvert.SerializeObject(new { error = $"Prefab not found at '{assetPath}'" });
}
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
if (!string.IsNullOrEmpty(instanceName))
{
instance.name = instanceName;
}
if (!string.IsNullOrEmpty(parentName))
{
var parent = GameObject.Find(parentName) ?? FindGameObjectByPathDynamic(parentName);
if (parent != null)
{
instance.transform.SetParent(parent.transform, false);
}
}
ApplyTransformFromParametersDynamic(instance, parameters);
Undo.RegisterCreatedObjectUndo(instance, $"Instantiate {prefab.name}");
Selection.activeGameObject = instance;
return JsonConvert.SerializeObject(new {
success = true,
message = $"Instantiated prefab '{prefab.name}'",
instance = new {
name = instance.name,
prefabPath = assetPath,
path = GetGameObjectPathDynamic(instance)
}
});
}
private string LoadDynamicScene(Dictionary<string, string> parameters)
{
var sceneName = parameters.GetValueOrDefault("scene", "");
bool.TryParse(parameters.GetValueOrDefault("additive", "false"), out bool additive);
if (string.IsNullOrEmpty(sceneName))
{
return JsonConvert.SerializeObject(new { error = "Scene name required" });
}
string scenePath = sceneName;
if (!sceneName.EndsWith(".unity"))
{
var guids = AssetDatabase.FindAssets($"{sceneName} t:Scene");
if (guids.Length > 0)
{
scenePath = AssetDatabase.GUIDToAssetPath(guids[0]);
}
else
{
foreach (var buildScene in EditorBuildSettings.scenes)
{
if (Path.GetFileNameWithoutExtension(buildScene.path).Equals(sceneName, StringComparison.OrdinalIgnoreCase))
{
scenePath = buildScene.path;
break;
}
}
}
}
if (!File.Exists(scenePath))
{
return JsonConvert.SerializeObject(new { error = $"Scene '{sceneName}' not found" });
}
var mode = additive ? UnityEditor.SceneManagement.OpenSceneMode.Additive : UnityEditor.SceneManagement.OpenSceneMode.Single;
var scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(scenePath, mode);
return JsonConvert.SerializeObject(new {
success = true,
message = $"Loaded scene '{scene.name}' {(additive ? "additively" : "")}",
scene = new {
name = scene.name,
path = scene.path,
rootCount = scene.rootCount
}
});
}
private string AddDynamicComponent(Dictionary<string, string> parameters)
{
var gameObjectName = parameters.GetValueOrDefault("gameObject", "");
var componentType = parameters.GetValueOrDefault("component", "");
if (string.IsNullOrEmpty(gameObjectName))
{
return JsonConvert.SerializeObject(new { error = "GameObject name required" });
}
if (string.IsNullOrEmpty(componentType))
{
return JsonConvert.SerializeObject(new { error = "Component type required" });
}
var gameObject = GameObject.Find(gameObjectName) ?? FindGameObjectByPathDynamic(gameObjectName);
if (gameObject == null)
{
return JsonConvert.SerializeObject(new { error = $"GameObject '{gameObjectName}' not found" });
}
var type = FindComponentTypeDynamic(componentType);
if (type == null)
{
return JsonConvert.SerializeObject(new { error = $"Component type '{componentType}' not found" });
}
if (gameObject.GetComponent(type) != null)
{
return JsonConvert.SerializeObject(new {
success = true,
message = $"Component '{componentType}' already exists on '{gameObjectName}'",
alreadyExists = true
});
}
var component = Undo.AddComponent(gameObject, type);
return JsonConvert.SerializeObject(new {
success = true,
message = $"Added component '{type.Name}' to '{gameObjectName}'",
component = new {
type = type.Name,
fullType = type.FullName
}
});
}
private void ApplyTransformFromParametersDynamic(GameObject go, Dictionary<string, string> parameters)
{
if (parameters.TryGetValue("position", out var posJson))
{
try
{
var pos = JsonConvert.DeserializeObject<Dictionary<string, float>>(posJson);
if (pos != null)
{
go.transform.position = new Vector3(
pos.GetValueOrDefault("x", 0),
pos.GetValueOrDefault("y", 0),
pos.GetValueOrDefault("z", 0)
);
}
}
catch { }
}
if (parameters.TryGetValue("rotation", out var rotJson))
{
try
{
var rot = JsonConvert.DeserializeObject<Dictionary<string, float>>(rotJson);
if (rot != null)
{
go.transform.eulerAngles = new Vector3(
rot.GetValueOrDefault("x", 0),
rot.GetValueOrDefault("y", 0),
rot.GetValueOrDefault("z", 0)
);
}
}
catch { }
}
if (parameters.TryGetValue("scale", out var scaleJson))
{
try
{
var scale = JsonConvert.DeserializeObject<Dictionary<string, float>>(scaleJson);
if (scale != null)
{
go.transform.localScale = new Vector3(
scale.GetValueOrDefault("x", 1),
scale.GetValueOrDefault("y", 1),
scale.GetValueOrDefault("z", 1)
);
}
}
catch { }
}
}
private string GetGameObjectPathDynamic(GameObject go)
{
string path = go.name;
Transform current = go.transform.parent;
while (current != null)
{
path = current.name + "/" + path;
current = current.parent;
}
return path;
}
#endregion
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: ed1edec647066482b8f641d3e27f7721
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusExecutorDynamicTools.cs
uploadId: 920982
@@ -0,0 +1,139 @@
using System;
using UnityEditor;
using UnityEngine;
namespace SynapticPro
{
/// <summary>
/// Window to manage operation history and Undo/Redo
/// </summary>
public class NexusHistoryWindow : EditorWindow
{
// [MenuItem("Window/Synaptic Pro/📜 Operation History")]
public static void ShowWindow()
{
var window = GetWindow<NexusHistoryWindow>("📜 Operation History");
window.minSize = new Vector2(300, 400);
window.Show();
}
private Vector2 scrollPosition;
private bool autoRefresh = true;
private void OnEnable()
{
NexusOperationHistory.Instance.OnHistoryChanged += Repaint;
}
private void OnDisable()
{
NexusOperationHistory.Instance.OnHistoryChanged -= Repaint;
}
private void OnGUI()
{
DrawHeader();
DrawControls();
DrawHistoryInfo();
}
private void DrawHeader()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Label("Operation History", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
autoRefresh = GUILayout.Toggle(autoRefresh, "Auto Refresh", EditorStyles.toolbarButton, GUILayout.Width(90));
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
Repaint();
}
EditorGUILayout.EndHorizontal();
}
private void DrawControls()
{
EditorGUILayout.BeginHorizontal();
// Undo button
GUI.enabled = NexusOperationHistory.Instance.CanUndo;
if (GUILayout.Button("↶ Undo", GUILayout.Height(30)))
{
if (NexusOperationHistory.Instance.Undo())
{
EditorUtility.DisplayDialog("Undo", "Operation undone", "OK");
}
}
// Redo button
GUI.enabled = NexusOperationHistory.Instance.CanRedo;
if (GUILayout.Button("↷ Redo", GUILayout.Height(30)))
{
if (NexusOperationHistory.Instance.Redo())
{
EditorUtility.DisplayDialog("Redo", "Operation redone", "OK");
}
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
// Clear history
if (GUILayout.Button("Clear History", GUILayout.Height(25)))
{
if (EditorUtility.DisplayDialog("Confirm", "Delete all history?", "Delete", "Cancel"))
{
NexusOperationHistory.Instance.ClearHistory();
}
}
// Export
if (GUILayout.Button("Export", GUILayout.Height(25)))
{
ExportHistory();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10);
}
private void DrawHistoryInfo()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
var info = NexusOperationHistory.Instance.GetHistoryInfo();
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
EditorGUILayout.TextArea(info, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
private void ExportHistory()
{
var path = EditorUtility.SaveFilePanel(
"Export History",
Application.dataPath,
$"NexusHistory_{DateTime.Now:yyyyMMdd_HHmmss}.json",
"json"
);
if (!string.IsNullOrEmpty(path))
{
var json = NexusOperationHistory.Instance.ExportHistory();
System.IO.File.WriteAllText(path, json);
EditorUtility.DisplayDialog("Export Complete", "History has been exported", "OK");
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 938cf1b3a071149eba7cc38716a66904
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusHistoryWindow.cs
uploadId: 920982
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 004694d47152b4d8bbc100abd35eaf4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusMainWindow.cs
uploadId: 920982
@@ -0,0 +1,479 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using SynapticAIPro;
using Newtonsoft.Json;
namespace SynapticPro
{
/// <summary>
/// Project planning and Todo management window
/// Displays and manages AI-generated plans
/// </summary>
public class NexusProjectPlannerWindow : EditorWindow
{
// [MenuItem("Window/Synaptic Pro/📋 Project Planner")]
public static void ShowWindow()
{
var window = GetWindow<NexusProjectPlannerWindow>("📋 Project Planner");
window.minSize = new Vector2(600, 400);
window.Show();
}
private Vector2 scrollPosition;
private ProjectPlan currentPlan;
private List<ProjectTask> tasks = new List<ProjectTask>();
private string newTaskInput = "";
private int selectedTaskIndex = -1;
// Styles
private GUIStyle headerStyle;
private GUIStyle taskStyle;
private GUIStyle completedTaskStyle;
private GUIStyle phaseStyle;
[Serializable]
public class ProjectPlan
{
public string title = "New Project";
public string overview = "";
public List<ProjectPhase> phases = new List<ProjectPhase>();
public string currentPhase = "planning";
public float progress = 0f;
}
[Serializable]
public class ProjectPhase
{
public string name;
public List<string> tasks;
public bool isCompleted;
}
[Serializable]
public class ProjectTask
{
public int id;
public string name;
public string description;
public string status = "pending"; // pending, in_progress, completed
public string priority = "medium"; // low, medium, high
public List<ProjectTask> subtasks;
public DateTime createdAt;
public DateTime? completedAt;
}
private void OnEnable()
{
// Setup WebSocket message reception
NexusWebSocketClient.Instance.OnMessageReceived += OnWebSocketMessage;
// Load saved data
LoadProjectData();
}
private void OnDisable()
{
NexusWebSocketClient.Instance.OnMessageReceived -= OnWebSocketMessage;
// Save data
SaveProjectData();
}
private void InitializeStyles()
{
headerStyle = new GUIStyle(EditorStyles.largeLabel)
{
fontSize = 20,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleCenter
};
taskStyle = new GUIStyle(EditorStyles.label)
{
fontSize = 14,
padding = new RectOffset(20, 10, 5, 5),
wordWrap = true
};
completedTaskStyle = new GUIStyle(taskStyle)
{
normal = { textColor = new Color(0.5f, 0.5f, 0.5f) },
fontStyle = FontStyle.Italic
};
phaseStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 16,
padding = new RectOffset(10, 10, 10, 5)
};
}
private void OnGUI()
{
if (headerStyle == null)
InitializeStyles();
DrawHeader();
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
if (currentPlan != null)
{
DrawProjectOverview();
DrawPhases();
}
DrawTaskList();
DrawTaskInput();
EditorGUILayout.EndScrollView();
}
private void DrawHeader()
{
EditorGUILayout.Space(10);
GUILayout.Label("📋 Project Planner", headerStyle, GUILayout.Height(30));
if (currentPlan != null)
{
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
// Progress bar
var rect = GUILayoutUtility.GetRect(300, 20);
EditorGUI.ProgressBar(rect, currentPlan.progress, $"Progress: {currentPlan.progress * 100:F0}%");
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space(10);
}
private void DrawProjectOverview()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// Project title
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Project:", GUILayout.Width(80));
currentPlan.title = EditorGUILayout.TextField(currentPlan.title);
EditorGUILayout.EndHorizontal();
// Overview
GUILayout.Label("Overview:", EditorStyles.boldLabel);
currentPlan.overview = EditorGUILayout.TextArea(currentPlan.overview, GUILayout.Height(60));
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
}
private void DrawPhases()
{
if (currentPlan.phases.Count == 0) return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("📅 Development Phases", EditorStyles.boldLabel);
foreach (var phase in currentPlan.phases)
{
EditorGUILayout.BeginHorizontal();
// Checkbox
bool wasCompleted = phase.isCompleted;
phase.isCompleted = EditorGUILayout.Toggle(phase.isCompleted, GUILayout.Width(20));
if (wasCompleted != phase.isCompleted)
{
UpdateProgress();
}
// Phase name
var style = phase.isCompleted ? completedTaskStyle : phaseStyle;
GUILayout.Label(phase.name, style);
EditorGUILayout.EndHorizontal();
// Phase tasks
if (phase.tasks != null && phase.tasks.Count > 0)
{
EditorGUI.indentLevel++;
foreach (var task in phase.tasks)
{
EditorGUILayout.LabelField($"• {task}", EditorStyles.miniLabel);
}
EditorGUI.indentLevel--;
}
EditorGUILayout.Space(5);
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
}
private void DrawTaskList()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("✅ Task List", EditorStyles.boldLabel);
if (tasks.Count == 0)
{
EditorGUILayout.HelpBox("No tasks available. Try telling AI \"I want to create something like...\"", MessageType.Info);
}
else
{
for (int i = 0; i < tasks.Count; i++)
{
DrawTask(tasks[i], i, 0);
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
}
private void DrawTask(ProjectTask task, int index, int indent)
{
EditorGUILayout.BeginHorizontal();
// Indent
GUILayout.Space(indent * 20);
// Checkbox
bool wasCompleted = task.status == "completed";
bool isCompleted = EditorGUILayout.Toggle(wasCompleted, GUILayout.Width(20));
if (wasCompleted != isCompleted)
{
task.status = isCompleted ? "completed" : "pending";
task.completedAt = isCompleted ? DateTime.Now : (DateTime?)null;
UpdateProgress();
}
// Priority indicator
var priorityColor = task.priority == "high" ? Color.red :
task.priority == "medium" ? Color.yellow :
Color.green;
var oldColor = GUI.color;
GUI.color = priorityColor;
GUILayout.Label("●", GUILayout.Width(15));
GUI.color = oldColor;
// Task name
var style = task.status == "completed" ? completedTaskStyle : taskStyle;
if (GUILayout.Button(task.name, style))
{
selectedTaskIndex = index;
}
// Status
var statusIcon = task.status == "completed" ? "✅" :
task.status == "in_progress" ? "🔄" : "⏳";
GUILayout.Label(statusIcon, GUILayout.Width(25));
// Delete button
if (GUILayout.Button("×", GUILayout.Width(20)))
{
tasks.Remove(task);
}
EditorGUILayout.EndHorizontal();
// Selected task details
if (selectedTaskIndex == index && !string.IsNullOrEmpty(task.description))
{
EditorGUI.indentLevel++;
EditorGUILayout.HelpBox(task.description, MessageType.None);
EditorGUI.indentLevel--;
}
// Subtasks
if (task.subtasks != null && task.subtasks.Count > 0)
{
foreach (var subtask in task.subtasks)
{
DrawTask(subtask, -1, indent + 1);
}
}
}
private void DrawTaskInput()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
GUILayout.Label("New Task:", GUILayout.Width(80));
newTaskInput = EditorGUILayout.TextField(newTaskInput);
if (GUILayout.Button("Add", GUILayout.Width(60)))
{
if (!string.IsNullOrEmpty(newTaskInput))
{
AddTask(newTaskInput);
newTaskInput = "";
GUI.FocusControl(null);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private void AddTask(string taskName, string priority = "medium")
{
var task = new ProjectTask
{
id = tasks.Count + 1,
name = taskName,
status = "pending",
priority = priority,
createdAt = DateTime.Now,
subtasks = new List<ProjectTask>()
};
tasks.Add(task);
SaveProjectData();
}
private void OnWebSocketMessage(string message)
{
try
{
var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(message);
if (data != null && data.ContainsKey("type"))
{
var type = data["type"].ToString();
switch (type)
{
case "project_plan":
UpdateProjectPlan(data);
break;
case "task_list":
UpdateTaskList(data);
break;
case "task_update":
UpdateTask(data);
break;
}
}
}
catch (Exception e)
{
Debug.LogError($"[Project Planner] Error processing message: {e.Message}");
}
}
private void UpdateProjectPlan(Dictionary<string, object> data)
{
if (data.ContainsKey("plan"))
{
var planData = data["plan"] as Newtonsoft.Json.Linq.JObject;
if (planData != null)
{
currentPlan = planData.ToObject<ProjectPlan>();
Repaint();
}
}
}
private void UpdateTaskList(Dictionary<string, object> data)
{
if (data.ContainsKey("tasks"))
{
var tasksData = data["tasks"] as Newtonsoft.Json.Linq.JArray;
if (tasksData != null)
{
tasks = tasksData.ToObject<List<ProjectTask>>();
Repaint();
}
}
}
private void UpdateTask(Dictionary<string, object> data)
{
if (data.ContainsKey("taskId") && data.ContainsKey("status"))
{
int taskId = Convert.ToInt32(data["taskId"]);
string status = data["status"].ToString();
var task = tasks.Find(t => t.id == taskId);
if (task != null)
{
task.status = status;
if (status == "completed")
{
task.completedAt = DateTime.Now;
}
UpdateProgress();
Repaint();
}
}
}
private void UpdateProgress()
{
if (currentPlan != null)
{
int totalTasks = tasks.Count;
int completedTasks = tasks.FindAll(t => t.status == "completed").Count;
if (totalTasks > 0)
{
currentPlan.progress = (float)completedTasks / totalTasks;
}
// Also consider phase progress
if (currentPlan.phases.Count > 0)
{
int completedPhases = currentPlan.phases.FindAll(p => p.isCompleted).Count;
float phaseProgress = (float)completedPhases / currentPlan.phases.Count;
// Overall progress is average of task progress and phase progress
currentPlan.progress = (currentPlan.progress + phaseProgress) / 2f;
}
}
}
private void LoadProjectData()
{
// Load saved data from EditorPrefs
string planJson = EditorPrefs.GetString("NexusProjectPlan", "");
if (!string.IsNullOrEmpty(planJson))
{
currentPlan = JsonConvert.DeserializeObject<ProjectPlan>(planJson);
}
else
{
currentPlan = new ProjectPlan();
}
string tasksJson = EditorPrefs.GetString("NexusProjectTasks", "");
if (!string.IsNullOrEmpty(tasksJson))
{
tasks = JsonConvert.DeserializeObject<List<ProjectTask>>(tasksJson);
}
}
private void SaveProjectData()
{
// Save to EditorPrefs
if (currentPlan != null)
{
EditorPrefs.SetString("NexusProjectPlan", JsonConvert.SerializeObject(currentPlan));
}
if (tasks != null && tasks.Count > 0)
{
EditorPrefs.SetString("NexusProjectTasks", JsonConvert.SerializeObject(tasks));
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 8b169d81e845c4bd8b84f19675d03062
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusProjectPlannerWindow.cs
uploadId: 920982
@@ -0,0 +1,420 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using SynapticAIPro;
using UnityEditor;
using Newtonsoft.Json;
namespace SynapticPro
{
/// <summary>
/// Port management system per project
/// Assigns and manages unique ports for each Unity project
/// </summary>
[InitializeOnLoad]
public static class NexusProjectPortManager
{
private static string projectId;
private static int assignedPort = -1;
private static readonly string MAPPING_FILE_PATH;
// Project-port mapping information
[Serializable]
public class ProjectPortMapping
{
public Dictionary<string, ProjectInfo> projects = new Dictionary<string, ProjectInfo>();
}
[Serializable]
public class ProjectInfo
{
public string projectName;
public string projectPath;
public int port;
public DateTime lastUpdated;
public bool isActive;
}
static NexusProjectPortManager()
{
// Mapping file path (saved in user home directory)
string homeDir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
MAPPING_FILE_PATH = Path.Combine(homeDir, ".config", "nexus", "project_port_mapping.json");
// Initialize
Initialize();
}
private static void Initialize()
{
// Generate project ID (hash value of project path)
string projectPath = Application.dataPath;
projectId = GetProjectId(projectPath);
SynLog.Info($"[Nexus Port Manager] Project ID: {projectId}");
SynLog.Info($"[Nexus Port Manager] Project Path: {projectPath}");
// Assign port
AssignPort();
// Update status periodically
EditorApplication.update += UpdateProjectStatus;
EditorApplication.quitting += OnEditorQuitting;
}
/// <summary>
/// Generate project ID
/// </summary>
private static string GetProjectId(string projectPath)
{
// Generate unique ID from project path
using (var md5 = System.Security.Cryptography.MD5.Create())
{
byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(projectPath);
byte[] hashBytes = md5.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant().Substring(0, 8);
}
}
/// <summary>
/// Assign port
/// </summary>
private static void AssignPort()
{
var mapping = LoadMapping();
// Check existing port assignment
if (mapping.projects.ContainsKey(projectId))
{
var info = mapping.projects[projectId];
assignedPort = info.port;
info.lastUpdated = DateTime.Now;
info.isActive = true;
info.projectName = PlayerSettings.productName;
SynLog.Info($"[Nexus Port Manager] Using existing port: {assignedPort}");
}
else
{
// Assign new port
assignedPort = FindAvailablePort(mapping);
mapping.projects[projectId] = new ProjectInfo
{
projectName = PlayerSettings.productName,
projectPath = Application.dataPath,
port = assignedPort,
lastUpdated = DateTime.Now,
isActive = true
};
SynLog.Info($"[Nexus Port Manager] Assigned new port: {assignedPort}");
}
SaveMapping(mapping);
// Notify port to NexusEditorMCPService
UpdateMCPServicePort();
}
/// <summary>
/// Find available port
/// </summary>
private static int FindAvailablePort(ProjectPortMapping mapping)
{
int[] candidatePorts = { 8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099 };
foreach (int port in candidatePorts)
{
bool isUsed = false;
foreach (var project in mapping.projects.Values)
{
if (project.port == port && project.isActive)
{
isUsed = true;
break;
}
}
if (!isUsed)
{
return port;
}
}
// If all in use, search sequentially from 8090
return 8090 + mapping.projects.Count;
}
/// <summary>
/// Set port for MCP service
/// </summary>
private static void UpdateMCPServicePort()
{
if (assignedPort > 0)
{
// Check actual port of current MCP server
string currentUrl = NexusEditorMCPService.GetServerUrl();
if (!string.IsNullOrEmpty(currentUrl) && currentUrl.Contains("localhost:"))
{
// Get actually running port
int startIndex = currentUrl.IndexOf("localhost:") + "localhost:".Length;
int endIndex = currentUrl.IndexOf("/", startIndex);
if (endIndex == -1) endIndex = currentUrl.Length;
if (int.TryParse(currentUrl.Substring(startIndex, endIndex - startIndex), out int actualPort))
{
// If actual port differs from assigned port, prioritize actual port
if (actualPort != assignedPort && IsPortInUse(actualPort))
{
SynLog.Info($"[Nexus Port Manager] MCP Server is actually running on port {actualPort}, not {assignedPort}. Keeping actual port.");
assignedPort = actualPort;
// Update mapping
var mapping = LoadMapping();
if (mapping.projects.ContainsKey(projectId))
{
mapping.projects[projectId].port = actualPort;
SaveMapping(mapping);
}
return;
}
}
}
// Only use saved port if actual server not found
string serverUrl = $"ws://localhost:{assignedPort}";
NexusEditorMCPService.SetServerUrl(serverUrl);
SynLog.Info($"[Nexus Port Manager] Updated MCP Service URL: {serverUrl}");
}
}
/// <summary>
/// Check if specified port is in use
/// </summary>
private static bool IsPortInUse(int port)
{
try
{
using (var tcpClient = new System.Net.Sockets.TcpClient())
{
var result = tcpClient.BeginConnect("localhost", port, null, null);
bool success = result.AsyncWaitHandle.WaitOne(100);
if (success)
{
tcpClient.EndConnect(result);
tcpClient.Close();
return true;
}
return false;
}
}
catch
{
return false;
}
}
/// <summary>
/// Update project status
/// </summary>
private static void UpdateProjectStatus()
{
// Update status every 5 minutes
if (EditorApplication.timeSinceStartup % 300 < 1)
{
var mapping = LoadMapping();
if (mapping.projects.ContainsKey(projectId))
{
mapping.projects[projectId].lastUpdated = DateTime.Now;
mapping.projects[projectId].isActive = true;
SaveMapping(mapping);
}
}
}
/// <summary>
/// Processing on editor exit
/// </summary>
private static void OnEditorQuitting()
{
var mapping = LoadMapping();
if (mapping.projects.ContainsKey(projectId))
{
mapping.projects[projectId].isActive = false;
SaveMapping(mapping);
}
EditorApplication.update -= UpdateProjectStatus;
}
/// <summary>
/// Load mapping information
/// </summary>
private static ProjectPortMapping LoadMapping()
{
try
{
if (File.Exists(MAPPING_FILE_PATH))
{
string json = File.ReadAllText(MAPPING_FILE_PATH);
// Validate and clean JSON string
json = json.Trim();
if (string.IsNullOrEmpty(json))
{
SynLog.Warn("[Nexus Port Manager] Mapping file is empty, creating new mapping.");
return new ProjectPortMapping();
}
// Check JSON syntax
if (!json.StartsWith("{") || !json.EndsWith("}"))
{
Debug.LogError($"[Nexus Port Manager] Invalid JSON format in mapping file. Content: {json.Substring(0, Math.Min(100, json.Length))}...");
return new ProjectPortMapping();
}
var result = JsonConvert.DeserializeObject<ProjectPortMapping>(json);
return result ?? new ProjectPortMapping();
}
}
catch (JsonException jsonEx)
{
Debug.LogError($"[Nexus Port Manager] JSON parsing error: {jsonEx.Message}");
Debug.LogError($"[Nexus Port Manager] Recreating mapping file due to corruption.");
// Move-aside corrupted file, then write a fresh empty mapping so
// the next LoadMapping doesn't hit the same corrupt content and
// spam the console at frame rate.
// Bugs the previous implementation had:
// 1. `File.Move` to an existing `.backup` throws on Windows,
// the silent catch left the corrupt file in place → loop.
// 2. Even on success, no fresh file was written, so any later
// reader before a SaveMapping run kept hitting the error
// (relevant when MAPPING_FILE_PATH still existed under
// filesystem latency on slow disks).
try
{
string backupPath = MAPPING_FILE_PATH + ".backup";
if (File.Exists(backupPath))
{
try { File.Delete(backupPath); } catch { }
}
if (File.Exists(MAPPING_FILE_PATH))
{
try { File.Move(MAPPING_FILE_PATH, backupPath); }
catch
{
// Move failed (permissions, fs lock) — delete the
// corrupt source outright so we don't loop.
try { File.Delete(MAPPING_FILE_PATH); } catch { }
}
}
// Immediately materialize a clean mapping file so concurrent
// readers (and the next Update tick) see valid JSON.
var fresh = new ProjectPortMapping();
var freshJson = JsonConvert.SerializeObject(fresh, Formatting.Indented);
File.WriteAllText(MAPPING_FILE_PATH, freshJson);
SynLog.Info($"[Nexus Port Manager] Corrupted file backed up to: {backupPath}");
return fresh;
}
catch (Exception writeErr)
{
Debug.LogError($"[Nexus Port Manager] Failed to reset mapping file: {writeErr.Message}");
return new ProjectPortMapping();
}
}
catch (Exception e)
{
Debug.LogError($"[Nexus Port Manager] Failed to load mapping: {e.Message}");
}
return new ProjectPortMapping();
}
/// <summary>
/// Save mapping information
/// </summary>
private static void SaveMapping(ProjectPortMapping mapping)
{
try
{
// Clean up old entries (not updated for more than 30 days)
var keysToRemove = new List<string>();
foreach (var kvp in mapping.projects)
{
if ((DateTime.Now - kvp.Value.lastUpdated).TotalDays > 30)
{
keysToRemove.Add(kvp.Key);
}
}
foreach (var key in keysToRemove)
{
mapping.projects.Remove(key);
}
// Create directory
string directory = Path.GetDirectoryName(MAPPING_FILE_PATH);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// Save
string json = JsonConvert.SerializeObject(mapping, Formatting.Indented);
File.WriteAllText(MAPPING_FILE_PATH, json);
}
catch (Exception e)
{
Debug.LogError($"[Nexus Port Manager] Failed to save mapping: {e.Message}");
}
}
/// <summary>
/// Get port for current project
/// </summary>
public static int GetAssignedPort()
{
return assignedPort;
}
/// <summary>
/// Get current project ID
/// </summary>
public static string GetProjectId()
{
return projectId;
}
/// <summary>
/// Display mapping information (for debugging)
/// </summary>
[MenuItem("Tools/Synaptic Pro/Show Port Mapping")]
public static void ShowPortMapping()
{
var mapping = LoadMapping();
System.Text.StringBuilder info = new System.Text.StringBuilder();
info.AppendLine("🔌 Nexus Project Port Mapping");
info.AppendLine("================================");
info.AppendLine($"Current Project ID: {projectId}");
info.AppendLine($"Assigned Port: {assignedPort}");
info.AppendLine("\nAll Projects:");
foreach (var kvp in mapping.projects)
{
var project = kvp.Value;
info.AppendLine($"\n📁 {project.projectName}");
info.AppendLine($" ID: {kvp.Key}");
info.AppendLine($" Port: {project.port}");
info.AppendLine($" Path: {project.projectPath}");
info.AppendLine($" Active: {project.isActive}");
info.AppendLine($" Last Updated: {project.lastUpdated}");
}
SynLog.Info(info.ToString());
EditorUtility.DisplayDialog("Port Mapping", info.ToString(), "OK");
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c59fc0a94d3554965805860d693abfee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusProjectPortManager.cs
uploadId: 920982
@@ -0,0 +1,655 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using SynapticAIPro;
using UnityEditor;
using System.Text;
using Newtonsoft.Json;
namespace SynapticPro
{
/// <summary>
/// Unity project settings detailed retrieval and manipulation system
/// Provides complete information for Build Settings, Player Settings, Quality Settings, etc.
/// </summary>
public static class NexusProjectSettings
{
#region Build Settings
/// <summary>
/// Get detailed information for Build Settings
/// </summary>
public static string GetBuildSettings()
{
try
{
var buildSettings = new Dictionary<string, object>();
// Basic settings
buildSettings["target_group"] = EditorUserBuildSettings.selectedBuildTargetGroup.ToString();
buildSettings["build_target"] = EditorUserBuildSettings.activeBuildTarget.ToString();
buildSettings["development_build"] = EditorUserBuildSettings.development;
buildSettings["auto_connect_profiler"] = EditorUserBuildSettings.connectProfiler;
buildSettings["deep_profiling"] = EditorUserBuildSettings.buildWithDeepProfilingSupport;
buildSettings["script_debugging"] = EditorUserBuildSettings.allowDebugging;
// Scene settings
var scenes = EditorBuildSettings.scenes;
var sceneList = new List<Dictionary<string, object>>();
for (int i = 0; i < scenes.Length; i++)
{
var scene = scenes[i];
sceneList.Add(new Dictionary<string, object>
{
["index"] = i,
["path"] = scene.path,
["enabled"] = scene.enabled,
["guid"] = scene.guid.ToString()
});
}
buildSettings["scenes"] = sceneList;
// Platform-specific settings
var platformSettings = new Dictionary<string, object>();
// Android settings
if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
{
platformSettings["android"] = new Dictionary<string, object>
{
["build_system"] = EditorUserBuildSettings.androidBuildSystem.ToString(),
["export_project"] = EditorUserBuildSettings.exportAsGoogleAndroidProject,
["build_app_bundle"] = EditorUserBuildSettings.buildAppBundle
};
}
// iOS settings
if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS)
{
platformSettings["ios"] = new Dictionary<string, object>
{
["build_number"] = PlayerSettings.iOS.buildNumber
};
}
// WebGL settings
if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.WebGL)
{
platformSettings["webgl"] = new Dictionary<string, object>
{
// WebGL specific settings would go here
};
}
buildSettings["platform_settings"] = platformSettings;
return JsonConvert.SerializeObject(buildSettings, Formatting.Indented);
}
catch (Exception e)
{
return $"Error getting build settings: {e.Message}";
}
}
#endregion
#region Player Settings
/// <summary>
/// Get detailed information for Player Settings
/// </summary>
public static string GetPlayerSettings()
{
try
{
var playerSettings = new Dictionary<string, object>();
// Basic information
playerSettings["company_name"] = PlayerSettings.companyName;
playerSettings["product_name"] = PlayerSettings.productName;
playerSettings["version"] = PlayerSettings.bundleVersion;
playerSettings["bundle_identifier"] = PlayerSettings.GetApplicationIdentifier(EditorUserBuildSettings.selectedBuildTargetGroup);
// Icon & Splash
// Default icon handling varies by platform
playerSettings["has_default_icon"] = true;
playerSettings["use_animated_autorotation"] = PlayerSettings.useAnimatedAutorotation;
// Resolution & Display
var resolutionSettings = new Dictionary<string, object>
{
["default_is_fullscreen"] = PlayerSettings.defaultIsNativeResolution,
["default_screen_width"] = PlayerSettings.defaultScreenWidth,
["default_screen_height"] = PlayerSettings.defaultScreenHeight,
["run_in_background"] = PlayerSettings.runInBackground,
["capture_single_screen"] = PlayerSettings.captureSingleScreen,
// Display resolution dialog removed in newer Unity versions
["use_player_log"] = PlayerSettings.usePlayerLog,
["resize_with_window"] = PlayerSettings.resizableWindow,
["visible_in_background"] = PlayerSettings.visibleInBackground
};
playerSettings["resolution_presentation"] = resolutionSettings;
// Splash Screen
var splashSettings = new Dictionary<string, object>
{
["show_unity_logo"] = PlayerSettings.SplashScreen.showUnityLogo,
["animation_mode"] = PlayerSettings.SplashScreen.animationMode.ToString(),
["background_color"] = ColorToHex(PlayerSettings.SplashScreen.backgroundColor),
["logo_style"] = PlayerSettings.SplashScreen.unityLogoStyle.ToString()
};
playerSettings["splash_screen"] = splashSettings;
// XR Settings
var xrSettings = new Dictionary<string, object>
{
// VR support moved to XR Management package
};
playerSettings["xr_settings"] = xrSettings;
// Publishing Settings
var publishingSettings = new Dictionary<string, object>
{
["use_mac_app_store_validation"] = PlayerSettings.useMacAppStoreValidation,
// Mac App Store category setting
};
playerSettings["publishing_settings"] = publishingSettings;
// Configuration
var configurationSettings = new Dictionary<string, object>
{
["scripting_backend"] = PlayerSettings.GetScriptingBackend(EditorUserBuildSettings.selectedBuildTargetGroup).ToString(),
["api_compatibility_level"] = PlayerSettings.GetApiCompatibilityLevel(EditorUserBuildSettings.selectedBuildTargetGroup).ToString(),
// Input handling setting varies by Unity version
["il2cpp_compiler_configuration"] = PlayerSettings.GetIl2CppCompilerConfiguration(EditorUserBuildSettings.selectedBuildTargetGroup).ToString()
};
playerSettings["configuration"] = configurationSettings;
// Platform-specific settings
var platformSpecific = GetPlatformSpecificPlayerSettings();
playerSettings["platform_specific"] = platformSpecific;
return JsonConvert.SerializeObject(playerSettings, Formatting.Indented);
}
catch (Exception e)
{
return $"Error getting player settings: {e.Message}";
}
}
private static Dictionary<string, object> GetPlatformSpecificPlayerSettings()
{
var platformSettings = new Dictionary<string, object>();
// Android-specific settings
var androidSettings = new Dictionary<string, object>
{
["bundle_version_code"] = PlayerSettings.Android.bundleVersionCode,
["min_sdk_version"] = PlayerSettings.Android.minSdkVersion.ToString(),
["target_sdk_version"] = PlayerSettings.Android.targetSdkVersion.ToString(),
["preferred_install_location"] = PlayerSettings.Android.preferredInstallLocation.ToString(),
["force_internet_permission"] = PlayerSettings.Android.forceInternetPermission,
["force_sd_card_permission"] = PlayerSettings.Android.forceSDCardPermission,
["keystore_name"] = PlayerSettings.Android.keystoreName,
["keystore_pass"] = "[PROTECTED]",
["keyalias_name"] = PlayerSettings.Android.keyaliasName,
["use_custom_keystore"] = PlayerSettings.Android.useCustomKeystore
};
platformSettings["android"] = androidSettings;
// iOS-specific settings
var iosSettings = new Dictionary<string, object>
{
["build_number"] = PlayerSettings.iOS.buildNumber,
["target_os_version"] = PlayerSettings.iOS.targetOSVersionString,
["camera_usage_description"] = PlayerSettings.iOS.cameraUsageDescription,
["location_usage_description"] = PlayerSettings.iOS.locationUsageDescription,
["microphone_usage_description"] = PlayerSettings.iOS.microphoneUsageDescription,
["requires_persistent_wifi"] = PlayerSettings.iOS.requiresPersistentWiFi,
// Exit on suspend deprecated, use appInBackgroundBehavior instead
["app_in_background_behavior"] = PlayerSettings.iOS.appInBackgroundBehavior.ToString()
};
platformSettings["ios"] = iosSettings;
return platformSettings;
}
#endregion
#region Quality Settings
/// <summary>
/// Get detailed information for Quality Settings
/// </summary>
public static string GetQualitySettings()
{
try
{
var qualitySettings = new Dictionary<string, object>();
// Current quality level
qualitySettings["current_level"] = QualitySettings.GetQualityLevel();
qualitySettings["current_level_name"] = QualitySettings.names[QualitySettings.GetQualityLevel()];
// All quality levels
var levels = new List<Dictionary<string, object>>();
string[] names = QualitySettings.names;
for (int i = 0; i < names.Length; i++)
{
// Temporarily switch level to retrieve settings
int currentLevel = QualitySettings.GetQualityLevel();
QualitySettings.SetQualityLevel(i, false);
var levelSettings = new Dictionary<string, object>
{
["index"] = i,
["name"] = names[i],
["pixel_light_count"] = QualitySettings.pixelLightCount,
#if UNITY_2022_2_OR_NEWER
["texture_quality"] = QualitySettings.globalTextureMipmapLimit,
#else
["texture_quality"] = QualitySettings.masterTextureLimit,
#endif
["anisotropic_textures"] = QualitySettings.anisotropicFiltering.ToString(),
["anti_aliasing"] = QualitySettings.antiAliasing,
["soft_particles"] = QualitySettings.softParticles,
["realtime_reflection_probes"] = QualitySettings.realtimeReflectionProbes,
["billboard_face_camera_position"] = QualitySettings.billboardsFaceCameraPosition,
["resolution_scaling_fixed_dpi_factor"] = QualitySettings.resolutionScalingFixedDPIFactor,
["texture_streaming_enabled"] = QualitySettings.streamingMipmapsActive,
["texture_streaming_memory_budget"] = QualitySettings.streamingMipmapsMemoryBudget,
["maximum_lod_bias"] = QualitySettings.maximumLODLevel,
["particle_raycast_budget"] = QualitySettings.particleRaycastBudget,
["async_upload_time_slice"] = QualitySettings.asyncUploadTimeSlice,
["async_upload_buffer_size"] = QualitySettings.asyncUploadBufferSize,
["async_upload_persistent_buffer"] = QualitySettings.asyncUploadPersistentBuffer,
["realtime_gi_cpu_usage"] = QualitySettings.realtimeGICPUUsage.ToString(),
["skinned_mesh_max_bone_count"] = QualitySettings.skinWeights.ToString()
};
levels.Add(levelSettings);
// Restore original level
QualitySettings.SetQualityLevel(currentLevel, false);
}
qualitySettings["levels"] = levels;
// Detail settings
var detailSettings = new Dictionary<string, object>
{
["blend_weights"] = QualitySettings.skinWeights.ToString(),
["vsync_count"] = QualitySettings.vSyncCount,
["lod_bias"] = QualitySettings.lodBias,
["maximum_lod_level"] = QualitySettings.maximumLODLevel,
["particle_raycast_budget"] = QualitySettings.particleRaycastBudget,
["soft_vegetation"] = QualitySettings.softVegetation
};
qualitySettings["detail_settings"] = detailSettings;
return JsonConvert.SerializeObject(qualitySettings, Formatting.Indented);
}
catch (Exception e)
{
return $"Error getting quality settings: {e.Message}";
}
}
#endregion
#region Input Settings
/// <summary>
/// Get detailed information for Input Settings
/// </summary>
public static string GetInputSettings()
{
try
{
var inputSettings = new Dictionary<string, object>();
// Input system settings
// Input handling setting varies by Unity version
inputSettings["input_system_available"] = true;
// Legacy Input Manager settings
var axes = new List<Dictionary<string, object>>();
// Get Axes settings from Input Manager (using Reflection)
var inputManagerAssets = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/InputManager.asset");
if (inputManagerAssets == null || inputManagerAssets.Length == 0)
{
SynLog.Warn("[GetInputSettings] InputManager asset not found");
inputSettings["input_axes"] = new List<Dictionary<string, object>>();
}
else
{
var inputManagerAsset = inputManagerAssets[0];
if (inputManagerAsset == null)
{
SynLog.Warn("[GetInputSettings] InputManager asset is null");
inputSettings["input_axes"] = new List<Dictionary<string, object>>();
}
else
{
var serializedObject = new SerializedObject(inputManagerAsset);
var axesProperty = serializedObject.FindProperty("m_Axes");
if (axesProperty == null)
{
SynLog.Warn("[GetInputSettings] m_Axes property not found");
inputSettings["input_axes"] = new List<Dictionary<string, object>>();
}
else
{
for (int i = 0; i < axesProperty.arraySize; i++)
{
try
{
var axis = axesProperty.GetArrayElementAtIndex(i);
if (axis != null)
{
var axisData = new Dictionary<string, object>
{
["name"] = axis.FindPropertyRelative("m_Name")?.stringValue ?? "",
["descriptive_name"] = axis.FindPropertyRelative("m_DescriptiveName")?.stringValue ?? "",
["descriptive_negative_name"] = axis.FindPropertyRelative("m_DescriptiveNegativeName")?.stringValue ?? "",
["negative_button"] = axis.FindPropertyRelative("m_NegativeButton")?.stringValue ?? "",
["positive_button"] = axis.FindPropertyRelative("m_PositiveButton")?.stringValue ?? "",
["alt_negative_button"] = axis.FindPropertyRelative("m_AltNegativeButton")?.stringValue ?? "",
["alt_positive_button"] = axis.FindPropertyRelative("m_AltPositiveButton")?.stringValue ?? "",
["gravity"] = axis.FindPropertyRelative("m_Gravity")?.floatValue ?? 0f,
["dead"] = axis.FindPropertyRelative("m_Dead")?.floatValue ?? 0f,
["sensitivity"] = axis.FindPropertyRelative("m_Sensitivity")?.floatValue ?? 1f,
["snap"] = axis.FindPropertyRelative("m_Snap")?.boolValue ?? false,
["invert"] = axis.FindPropertyRelative("m_Invert")?.boolValue ?? false,
["type"] = axis.FindPropertyRelative("m_Type")?.intValue ?? 0,
["axis"] = axis.FindPropertyRelative("m_Axis")?.intValue ?? 0,
["joy_num"] = axis.FindPropertyRelative("m_JoyNum")?.intValue ?? 0
};
axes.Add(axisData);
}
}
catch (Exception axisEx)
{
SynLog.Warn($"[GetInputSettings] Failed to read axis {i}: {axisEx.Message}");
}
}
inputSettings["input_axes"] = axes;
}
}
}
// New Input System information (if available)
#if UNITY_INPUT_SYSTEM_MODULE_ENABLED
try
{
inputSettings["new_input_system"] = new Dictionary<string, object>
{
["enabled"] = true,
["version"] = UnityEngine.InputSystem.InputSystem.version
};
}
catch
{
inputSettings["new_input_system"] = new Dictionary<string, object>
{
["enabled"] = false,
["error"] = "Input System package not available"
};
}
#else
inputSettings["new_input_system"] = new Dictionary<string, object>
{
["enabled"] = false,
["note"] = "Input System module not enabled"
};
#endif
return JsonConvert.SerializeObject(inputSettings, Formatting.Indented);
}
catch (Exception e)
{
return $"Error getting input settings: {e.Message}";
}
}
#endregion
#region Physics Settings
/// <summary>
/// Get detailed information for Physics Settings
/// </summary>
public static string GetPhysicsSettings()
{
try
{
var physicsSettings = new Dictionary<string, object>();
// Basic physics settings
physicsSettings["gravity"] = new Dictionary<string, object>
{
["x"] = Physics.gravity.x,
["y"] = Physics.gravity.y,
["z"] = Physics.gravity.z
};
// Default physics material varies by Unity version
physicsSettings["has_default_material"] = true;
physicsSettings["bounce_threshold"] = Physics.bounceThreshold;
physicsSettings["sleep_threshold"] = Physics.sleepThreshold;
physicsSettings["default_contact_offset"] = Physics.defaultContactOffset;
physicsSettings["default_solver_iterations"] = Physics.defaultSolverIterations;
physicsSettings["default_solver_velocity_iterations"] = Physics.defaultSolverVelocityIterations;
// Query settings
physicsSettings["queries_hit_backfaces"] = Physics.queriesHitBackfaces;
physicsSettings["queries_hit_triggers"] = Physics.queriesHitTriggers;
physicsSettings["auto_sync_transforms"] = Physics.autoSyncTransforms;
physicsSettings["reuse_collision_callbacks"] = Physics.reuseCollisionCallbacks;
// Layer collision matrix
var layerCollisionMatrix = new Dictionary<string, object>();
for (int i = 0; i < 32; i++)
{
var layerName = LayerMask.LayerToName(i);
if (!string.IsNullOrEmpty(layerName))
{
var collisions = new List<string>();
for (int j = 0; j < 32; j++)
{
if (!Physics.GetIgnoreLayerCollision(i, j))
{
var otherLayerName = LayerMask.LayerToName(j);
if (!string.IsNullOrEmpty(otherLayerName))
{
collisions.Add(otherLayerName);
}
}
}
layerCollisionMatrix[layerName] = collisions;
}
}
physicsSettings["layer_collision_matrix"] = layerCollisionMatrix;
// 2D Physics settings
var physics2DSettings = new Dictionary<string, object>
{
["gravity"] = new Dictionary<string, object>
{
["x"] = Physics2D.gravity.x,
["y"] = Physics2D.gravity.y
},
// Default physics material varies by Unity version
["has_default_material"] = true,
["velocity_iterations"] = Physics2D.velocityIterations,
["position_iterations"] = Physics2D.positionIterations,
["velocity_threshold"] = Physics2D.bounceThreshold,
["max_linear_correction"] = Physics2D.maxLinearCorrection,
["max_angular_correction"] = Physics2D.maxAngularCorrection,
["max_translation_speed"] = Physics2D.maxTranslationSpeed,
["max_rotation_speed"] = Physics2D.maxRotationSpeed,
["baumgarte_scale"] = Physics2D.baumgarteScale,
["baumgarte_time_of_impact_scale"] = Physics2D.baumgarteTOIScale,
["time_to_sleep"] = Physics2D.timeToSleep,
["linear_sleep_tolerance"] = Physics2D.linearSleepTolerance,
["angular_sleep_tolerance"] = Physics2D.angularSleepTolerance,
["auto_sync_transforms"] = Physics2D.autoSyncTransforms,
["reuse_collision_callbacks"] = Physics2D.reuseCollisionCallbacks,
// Auto simulation deprecated, use simulationMode instead
["queries_hit_triggers"] = Physics2D.queriesHitTriggers,
["queries_start_in_colliders"] = Physics2D.queriesStartInColliders,
["callbacks_on_disable"] = Physics2D.callbacksOnDisable
};
physicsSettings["physics_2d"] = physics2DSettings;
return JsonConvert.SerializeObject(physicsSettings, Formatting.Indented);
}
catch (Exception e)
{
return $"Error getting physics settings: {e.Message}";
}
}
#endregion
#region Utility Methods
private static string ColorToHex(Color color)
{
return $"#{ColorUtility.ToHtmlStringRGBA(color)}";
}
/// <summary>
/// Get summary of all project settings
/// </summary>
public static string GetProjectSettingsSummary()
{
try
{
var summary = new Dictionary<string, object>
{
["project_info"] = new Dictionary<string, object>
{
["product_name"] = PlayerSettings.productName,
["bundle_version"] = PlayerSettings.bundleVersion,
["company_name"] = PlayerSettings.companyName,
["bundle_id"] = PlayerSettings.GetApplicationIdentifier(EditorUserBuildSettings.selectedBuildTargetGroup)
},
["build_settings"] = new Dictionary<string, object>
{
["target_platform"] = EditorUserBuildSettings.activeBuildTarget.ToString(),
["development_build"] = EditorUserBuildSettings.development,
["scripting_backend"] = PlayerSettings.GetScriptingBackend(EditorUserBuildSettings.selectedBuildTargetGroup).ToString()
},
["quality_settings"] = new Dictionary<string, object>
{
["quality_level"] = QualitySettings.names[QualitySettings.GetQualityLevel()],
["vsync_count"] = QualitySettings.vSyncCount,
["anti_aliasing"] = QualitySettings.antiAliasing,
["aniso_filtering"] = QualitySettings.anisotropicFiltering.ToString()
},
["physics_settings"] = new Dictionary<string, object>
{
["gravity_3d"] = new Dictionary<string, float>
{
["x"] = Physics.gravity.x,
["y"] = Physics.gravity.y,
["z"] = Physics.gravity.z
},
["gravity_2d"] = new Dictionary<string, float>
{
["x"] = Physics2D.gravity.x,
["y"] = Physics2D.gravity.y
}
},
["statistics"] = GetProjectStatistics()
};
return JsonConvert.SerializeObject(summary, Formatting.Indented);
}
catch (Exception e)
{
return $"Error getting project settings summary: {e.Message}";
}
}
/// <summary>
/// Get project statistics information
/// </summary>
private static Dictionary<string, object> GetProjectStatistics()
{
var stats = new Dictionary<string, object>();
try
{
// Number of GameObjects (active scene only)
var rootObjects = UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects();
int totalGameObjects = 0;
foreach (var root in rootObjects)
{
totalGameObjects += CountGameObjectsRecursive(root.transform);
}
// Asset statistics
string[] allAssets = AssetDatabase.GetAllAssetPaths();
var assetTypes = new Dictionary<string, int>();
foreach (string assetPath in allAssets)
{
if (assetPath.StartsWith("Assets/"))
{
string extension = System.IO.Path.GetExtension(assetPath).ToLower();
if (!string.IsNullOrEmpty(extension))
{
if (assetTypes.ContainsKey(extension))
assetTypes[extension]++;
else
assetTypes[extension] = 1;
}
}
}
stats["gameobject_count"] = totalGameObjects;
stats["script_count"] = assetTypes.ContainsKey(".cs") ? assetTypes[".cs"] : 0;
stats["total_assets"] = allAssets.Where(p => p.StartsWith("Assets/")).Count();
stats["asset_types"] = assetTypes;
stats["memory_usage"] = new Dictionary<string, object>
{
["allocated_memory"] = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong(),
["reserved_memory"] = UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong(),
["mono_heap_size"] = UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong(),
["mono_used_size"] = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong()
};
}
catch (Exception e)
{
stats["error"] = e.Message;
}
return stats;
}
/// <summary>
/// Count GameObjects recursively
/// </summary>
private static int CountGameObjectsRecursive(Transform transform)
{
int count = 1; // Self
for (int i = 0; i < transform.childCount; i++)
{
count += CountGameObjectsRecursive(transform.GetChild(i));
}
return count;
}
#endregion
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 35ea6fbe798d849429bcb67fff074834
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusProjectSettings.cs
uploadId: 920982
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 45faed9ddab504d1296232dba2fc21f7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusSetupWindow.cs
uploadId: 920982
@@ -0,0 +1,910 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
#if UNITY_EDITOR
using UnityEditor;
#endif
using System.IO;
using Newtonsoft.Json;
namespace SynapticPro
{
/// <summary>
/// Unity project state inspection and reporting class
/// Enables AI to understand current implementation status
/// </summary>
public static class NexusStateInspector
{
/// <summary>
/// Get scene information
/// </summary>
public static string GetSceneInformation(bool includeHierarchy = true, int maxDepth = 3)
{
var scene = SceneManager.GetActiveScene();
var info = new System.Text.StringBuilder();
info.AppendLine($"🎬 Scene Information");
info.AppendLine($"Name: {scene.name}");
info.AppendLine($"Path: {scene.path}");
info.AppendLine($"Build Index: {scene.buildIndex}");
var rootObjects = scene.GetRootGameObjects();
info.AppendLine($"Root GameObject Count: {rootObjects.Length}");
if (includeHierarchy)
{
info.AppendLine("\n📊 Hierarchy Structure:");
foreach (var root in rootObjects)
{
info.Append(GetGameObjectHierarchy(root, 0, maxDepth));
}
}
return info.ToString();
}
/// <summary>
/// Get GameObject details
/// </summary>
public static string GetGameObjectDetails(string name)
{
var obj = GameObject.Find(name);
if (obj == null) return $"❌ GameObject '{name}' not found";
var info = new System.Text.StringBuilder();
info.AppendLine($"🎯 GameObject Details: {obj.name}");
info.AppendLine($"Path: {GetFullPath(obj)}");
info.AppendLine($"Tag: {obj.tag}");
info.AppendLine($"Layer: {LayerMask.LayerToName(obj.layer)} ({obj.layer})");
info.AppendLine($"Active: {obj.activeSelf}");
info.AppendLine($"Static: {obj.isStatic}");
var transform = obj.transform;
info.AppendLine($"\n📐 Transform:");
info.AppendLine($" Position: {transform.position}");
info.AppendLine($" Rotation: {transform.rotation.eulerAngles}");
info.AppendLine($" Scale: {transform.localScale}");
info.AppendLine($" Child Count: {transform.childCount}");
info.AppendLine($"\n🔧 Components:");
var components = obj.GetComponents<Component>();
foreach (var comp in components)
{
if (comp == null) continue;
var compType = comp.GetType();
info.AppendLine($" • {compType.Name}");
// Details of major components
if (comp is Rigidbody rb)
{
info.AppendLine($" - Mass: {rb.mass}");
info.AppendLine($" - Use Gravity: {rb.useGravity}");
info.AppendLine($" - Kinematic: {rb.isKinematic}");
}
else if (comp is Collider col)
{
info.AppendLine($" - Trigger: {col.isTrigger}");
info.AppendLine($" - Enabled: {col.enabled}");
}
else if (comp is Renderer rend)
{
info.AppendLine($" - Material Count: {rend.sharedMaterials.Length}");
info.AppendLine($" - Enabled: {rend.enabled}");
}
}
return info.ToString();
}
/// <summary>
/// Get project asset information
/// </summary>
public static string GetProjectAssets(string assetType = "all", string folder = "Assets")
{
var info = new System.Text.StringBuilder();
info.AppendLine($"📁 Asset Information ({folder})");
string searchFilter = assetType switch
{
"scripts" => "t:MonoScript",
"prefabs" => "t:Prefab",
"materials" => "t:Material",
"textures" => "t:Texture2D",
"audio" => "t:AudioClip",
_ => ""
};
string[] guids = AssetDatabase.FindAssets(searchFilter, new[] { folder });
var assetsByType = new Dictionary<string, List<string>>();
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var type = AssetDatabase.GetMainAssetTypeAtPath(path);
string typeName = type?.Name ?? "Unknown";
if (!assetsByType.ContainsKey(typeName))
assetsByType[typeName] = new List<string>();
assetsByType[typeName].Add(Path.GetFileName(path));
}
foreach (var kvp in assetsByType.OrderBy(k => k.Key))
{
info.AppendLine($"\n{kvp.Key} ({kvp.Value.Count} items):");
foreach (var asset in kvp.Value.Take(10))
{
info.AppendLine($" - {asset}");
}
if (kvp.Value.Count > 10)
{
info.AppendLine($" ... {kvp.Value.Count - 10} more");
}
}
return info.ToString();
}
/// <summary>
/// Get overall project statistics
/// </summary>
public static string GetProjectStatistics()
{
try
{
// Scene statistics
var scene = SceneManager.GetActiveScene();
var allGameObjects = GameObject.FindObjectsOfType<GameObject>();
// Component statistics
var componentCounts = new Dictionary<string, int>();
foreach (var go in allGameObjects)
{
foreach (var comp in go.GetComponents<Component>())
{
if (comp == null) continue;
string typeName = comp.GetType().Name;
componentCounts[typeName] = componentCounts.GetValueOrDefault(typeName, 0) + 1;
}
}
// Asset statistics
string[] allAssets = AssetDatabase.FindAssets("");
var scripts = allAssets.Count(g => AssetDatabase.GUIDToAssetPath(g).EndsWith(".cs"));
var prefabs = allAssets.Count(g => AssetDatabase.GUIDToAssetPath(g).EndsWith(".prefab"));
var materials = allAssets.Count(g => AssetDatabase.GUIDToAssetPath(g).EndsWith(".mat"));
var textures = allAssets.Count(g => AssetDatabase.GUIDToAssetPath(g).EndsWith(".png") ||
AssetDatabase.GUIDToAssetPath(g).EndsWith(".jpg"));
// Nexus-created objects
var nexusCreated = allGameObjects.Where(go => go.name.Contains("Nexus_") ||
go.name.Contains("HelloWorld") ||
go.name.Contains("UI")).ToList();
var statistics = new Dictionary<string, object>
{
["scene_info"] = new Dictionary<string, object>
{
["name"] = scene.name,
["path"] = scene.path,
["is_loaded"] = scene.isLoaded,
["is_dirty"] = scene.isDirty
},
["gameobject_statistics"] = new Dictionary<string, object>
{
["total_count"] = allGameObjects.Length,
["active_count"] = allGameObjects.Count(go => go.activeInHierarchy),
["inactive_count"] = allGameObjects.Count(go => !go.activeInHierarchy)
},
["component_statistics"] = componentCounts.OrderByDescending(k => k.Value).Take(10).ToDictionary(k => k.Key, k => k.Value),
["asset_statistics"] = new Dictionary<string, object>
{
["total_assets"] = allAssets.Length,
["scripts"] = scripts,
["prefabs"] = prefabs,
["materials"] = materials,
["textures"] = textures
},
["nexus_created_objects"] = nexusCreated.Take(10).Select(obj => new Dictionary<string, object>
{
["name"] = obj.name,
["type"] = obj.GetType().Name,
["active"] = obj.activeInHierarchy,
["tag"] = obj.tag,
["layer"] = obj.layer
}).ToList()
};
return JsonConvert.SerializeObject(statistics, Formatting.Indented);
}
catch (System.Exception e)
{
return $"Error getting project statistics: {e.Message}";
}
}
/// <summary>
/// Get camera information
/// </summary>
public static string GetCameraInformation()
{
try
{
var cameras = GameObject.FindObjectsOfType<Camera>();
var mainCam = Camera.main;
var cameraInfo = new Dictionary<string, object>
{
["camera_count"] = cameras.Length,
["main_camera"] = mainCam != null ? mainCam.name : null,
["cameras"] = cameras.Select(cam => new Dictionary<string, object>
{
["name"] = cam.name,
["enabled"] = cam.enabled,
["transform"] = new Dictionary<string, object>
{
["position"] = new Dictionary<string, float>
{
["x"] = cam.transform.position.x,
["y"] = cam.transform.position.y,
["z"] = cam.transform.position.z
},
["rotation"] = new Dictionary<string, float>
{
["x"] = cam.transform.rotation.eulerAngles.x,
["y"] = cam.transform.rotation.eulerAngles.y,
["z"] = cam.transform.rotation.eulerAngles.z
}
},
["camera_settings"] = new Dictionary<string, object>
{
["field_of_view"] = cam.fieldOfView,
["orthographic"] = cam.orthographic,
["orthographic_size"] = cam.orthographic ? cam.orthographicSize : null,
["depth"] = cam.depth,
["culling_mask"] = cam.cullingMask,
["background_color"] = new Dictionary<string, float>
{
["r"] = cam.backgroundColor.r,
["g"] = cam.backgroundColor.g,
["b"] = cam.backgroundColor.b,
["a"] = cam.backgroundColor.a
},
["clear_flags"] = cam.clearFlags.ToString(),
["near_clip_plane"] = cam.nearClipPlane,
["far_clip_plane"] = cam.farClipPlane
},
["render_settings"] = new Dictionary<string, object>
{
["render_texture"] = cam.targetTexture != null ? cam.targetTexture.name : null,
["allow_hdr"] = cam.allowHDR,
["allow_msaa"] = cam.allowMSAA,
["use_physical_properties"] = cam.usePhysicalProperties
},
["has_post_process"] = cam.GetComponent("PostProcessVolume") != null
}).ToList()
};
return JsonConvert.SerializeObject(cameraInfo, Formatting.Indented);
}
catch (System.Exception e)
{
return $"Error getting camera information: {e.Message}";
}
}
/// <summary>
/// Get terrain information
/// </summary>
public static string GetTerrainInformation()
{
try
{
var terrains = GameObject.FindObjectsOfType<Terrain>();
var terrainInfo = new Dictionary<string, object>
{
["terrain_count"] = terrains.Length,
["terrains"] = terrains.Select(terrain => {
var data = terrain.terrainData;
return new Dictionary<string, object>
{
["name"] = terrain.name,
["position"] = new Dictionary<string, float>
{
["x"] = terrain.transform.position.x,
["y"] = terrain.transform.position.y,
["z"] = terrain.transform.position.z
},
["terrain_data"] = new Dictionary<string, object>
{
["size"] = new Dictionary<string, float>
{
["x"] = data.size.x,
["y"] = data.size.y,
["z"] = data.size.z
},
["heightmap_resolution"] = data.heightmapResolution,
["detail_resolution"] = data.detailResolution,
["alphamap_resolution"] = data.alphamapResolution,
["base_map_resolution"] = data.baseMapResolution
},
["terrain_layers"] = new Dictionary<string, object>
{
["count"] = data.terrainLayers?.Length ?? 0,
["layers"] = data.terrainLayers != null ?
data.terrainLayers.Take(5).Where(layer => layer != null).Select(layer => new Dictionary<string, object>
{
["name"] = layer.name,
["diffuse_texture"] = layer.diffuseTexture?.name,
["normal_map"] = layer.normalMapTexture?.name,
["tile_size"] = new Dictionary<string, float>
{
["x"] = layer.tileSize.x,
["y"] = layer.tileSize.y
}
}).ToList() : new List<Dictionary<string, object>>()
},
["vegetation"] = new Dictionary<string, object>
{
["tree_prototype_count"] = data.treePrototypes?.Length ?? 0,
["detail_prototype_count"] = data.detailPrototypes?.Length ?? 0,
["tree_instances"] = data.treeInstanceCount
},
["settings"] = new Dictionary<string, object>
{
["pixel_error"] = terrain.heightmapPixelError,
["base_map_distance"] = terrain.basemapDistance,
["detail_object_distance"] = terrain.detailObjectDistance,
["tree_distance"] = terrain.treeDistance,
["tree_billboard_distance"] = terrain.treeBillboardDistance
}
};
}).ToList()
};
return JsonConvert.SerializeObject(terrainInfo, Formatting.Indented);
}
catch (System.Exception e)
{
return $"Error getting terrain information: {e.Message}";
}
}
/// <summary>
/// Get lighting information
/// </summary>
public static string GetLightingInformation()
{
try
{
var lights = GameObject.FindObjectsOfType<Light>();
var probes = GameObject.FindObjectsOfType<ReflectionProbe>();
var lightingInfo = new Dictionary<string, object>
{
["ambient_settings"] = new Dictionary<string, object>
{
["mode"] = RenderSettings.ambientMode.ToString(),
["intensity"] = RenderSettings.ambientIntensity,
["color"] = new Dictionary<string, float>
{
["r"] = RenderSettings.ambientLight.r,
["g"] = RenderSettings.ambientLight.g,
["b"] = RenderSettings.ambientLight.b,
["a"] = RenderSettings.ambientLight.a
},
["skybox"] = RenderSettings.skybox != null ? RenderSettings.skybox.name : null
},
["fog_settings"] = new Dictionary<string, object>
{
["enabled"] = RenderSettings.fog,
["mode"] = RenderSettings.fogMode.ToString(),
["color"] = new Dictionary<string, float>
{
["r"] = RenderSettings.fogColor.r,
["g"] = RenderSettings.fogColor.g,
["b"] = RenderSettings.fogColor.b,
["a"] = RenderSettings.fogColor.a
},
["density"] = RenderSettings.fogDensity,
["start_distance"] = RenderSettings.fogStartDistance,
["end_distance"] = RenderSettings.fogEndDistance
},
["lights"] = new Dictionary<string, object>
{
["count"] = lights.Length,
["light_list"] = lights.Select(light => new Dictionary<string, object>
{
["name"] = light.name,
["enabled"] = light.enabled,
["type"] = light.type.ToString(),
["color"] = new Dictionary<string, float>
{
["r"] = light.color.r,
["g"] = light.color.g,
["b"] = light.color.b,
["a"] = light.color.a
},
["intensity"] = light.intensity,
["range"] = light.range,
["spot_angle"] = light.spotAngle,
["shadows"] = light.shadows.ToString(),
["transform"] = light.type == LightType.Directional ?
new Dictionary<string, object>
{
["rotation"] = new Dictionary<string, float>
{
["x"] = light.transform.rotation.eulerAngles.x,
["y"] = light.transform.rotation.eulerAngles.y,
["z"] = light.transform.rotation.eulerAngles.z
}
} :
new Dictionary<string, object>
{
["position"] = new Dictionary<string, float>
{
["x"] = light.transform.position.x,
["y"] = light.transform.position.y,
["z"] = light.transform.position.z
}
}
}).ToList()
},
["reflection_probes"] = new Dictionary<string, object>
{
["count"] = probes.Length,
["probes"] = probes.Select(probe => new Dictionary<string, object>
{
["name"] = probe.name,
["enabled"] = probe.enabled,
["resolution"] = probe.resolution,
["hdr"] = probe.hdr,
["clear_flags"] = probe.clearFlags.ToString(),
["culling_mask"] = probe.cullingMask
}).ToList()
}
};
var settings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
};
return JsonConvert.SerializeObject(lightingInfo, Formatting.Indented, settings);
}
catch (System.Exception e)
{
return $"Error getting lighting information: {e.Message}";
}
}
/// <summary>
/// Get material information
/// </summary>
public static string GetMaterialInformation()
{
try
{
// Materials used in scene
var renderers = GameObject.FindObjectsOfType<Renderer>();
var usedMaterials = new HashSet<Material>();
foreach (var renderer in renderers)
{
foreach (var mat in renderer.sharedMaterials)
{
if (mat != null)
{
usedMaterials.Add(mat);
}
}
}
// Classify by shader
var materialsByShader = usedMaterials.GroupBy(m => m.shader.name);
// Material assets in project
var materialAssets = AssetDatabase.FindAssets("t:Material");
var materialInfo = new Dictionary<string, object>
{
["scene_materials"] = new Dictionary<string, object>
{
["total_count"] = usedMaterials.Count,
["by_shader"] = materialsByShader.Select(group => new Dictionary<string, object>
{
["shader_name"] = group.Key,
["count"] = group.Count(),
["materials"] = group.Take(5).Select(mat =>
{
var matData = new Dictionary<string, object>
{
["name"] = mat.name,
["shader"] = mat.shader.name,
["render_queue"] = mat.renderQueue,
["keywords"] = mat.shaderKeywords.ToList()
};
// Color properties
if (mat.HasProperty("_Color") || mat.HasProperty("_BaseColor"))
{
var color = mat.HasProperty("_Color") ? mat.GetColor("_Color") : mat.GetColor("_BaseColor");
matData["color"] = new Dictionary<string, float>
{
["r"] = color.r,
["g"] = color.g,
["b"] = color.b,
["a"] = color.a
};
}
// Texture properties
if (mat.HasProperty("_MainTex") || mat.HasProperty("_BaseMap"))
{
var tex = mat.HasProperty("_MainTex") ? mat.GetTexture("_MainTex") : mat.GetTexture("_BaseMap");
if (tex != null)
{
matData["main_texture"] = new Dictionary<string, object>
{
["name"] = tex.name,
["width"] = tex.width,
["height"] = tex.height
};
}
}
// PBR properties
var pbrData = new Dictionary<string, object>();
if (mat.HasProperty("_Metallic"))
{
pbrData["metallic"] = mat.GetFloat("_Metallic");
}
if (mat.HasProperty("_Glossiness"))
{
pbrData["smoothness"] = mat.GetFloat("_Glossiness");
}
else if (mat.HasProperty("_Smoothness"))
{
pbrData["smoothness"] = mat.GetFloat("_Smoothness");
}
if (mat.HasProperty("_BumpMap") || mat.HasProperty("_NormalMap"))
{
var normalMap = mat.HasProperty("_BumpMap") ? mat.GetTexture("_BumpMap") : mat.GetTexture("_NormalMap");
pbrData["has_normal_map"] = normalMap != null;
}
if (pbrData.Count > 0)
{
matData["pbr_properties"] = pbrData;
}
return matData;
}).ToList()
}).ToList()
},
["project_materials"] = new Dictionary<string, object>
{
["total_count"] = materialAssets.Length,
["paths"] = materialAssets.Take(20).Select(guid => AssetDatabase.GUIDToAssetPath(guid)).ToList()
}
};
return JsonConvert.SerializeObject(materialInfo, Formatting.Indented);
}
catch (System.Exception e)
{
return $"Error getting material information: {e.Message}";
}
}
/// <summary>
/// Get UI information
/// </summary>
public static string GetUIInformation()
{
#if UNITY_EDITOR
try
{
var canvases = GameObject.FindObjectsOfType<Canvas>();
var eventSystem = GameObject.FindObjectOfType<UnityEngine.EventSystems.EventSystem>();
var uiInfo = new Dictionary<string, object>
{
["canvas_count"] = canvases.Length,
["canvases"] = canvases.Select(canvas =>
{
// Aggregate UI elements
var buttons = canvas.GetComponentsInChildren<UnityEngine.UI.Button>(true);
var texts = canvas.GetComponentsInChildren<UnityEngine.UI.Text>(true);
var images = canvas.GetComponentsInChildren<UnityEngine.UI.Image>(true);
var inputFields = canvas.GetComponentsInChildren<UnityEngine.UI.InputField>(true);
var sliders = canvas.GetComponentsInChildren<UnityEngine.UI.Slider>(true);
var toggles = canvas.GetComponentsInChildren<UnityEngine.UI.Toggle>(true);
var scrollViews = canvas.GetComponentsInChildren<UnityEngine.UI.ScrollRect>(true);
return new Dictionary<string, object>
{
["name"] = canvas.name,
["enabled"] = canvas.enabled,
["render_mode"] = canvas.renderMode.ToString(),
["sorting_order"] = canvas.sortingOrder,
["sorting_layer"] = canvas.sortingLayerName,
["pixel_perfect"] = canvas.pixelPerfect,
["ui_elements"] = new Dictionary<string, object>
{
["buttons"] = new Dictionary<string, object>
{
["count"] = buttons.Length,
["items"] = buttons.Take(5).Select(btn => {
var btnText = btn.GetComponentInChildren<UnityEngine.UI.Text>();
return new Dictionary<string, object>
{
["name"] = btn.name,
["text"] = btnText != null ? btnText.text : null,
["interactable"] = btn.interactable,
["active"] = btn.gameObject.activeInHierarchy
};
}).ToList()
},
["texts"] = new Dictionary<string, object>
{
["count"] = texts.Length,
["items"] = texts.Take(5).Select(txt => new Dictionary<string, object>
{
["name"] = txt.name,
["text"] = txt.text,
["font"] = txt.font != null ? txt.font.name : null,
["font_size"] = txt.fontSize,
["color"] = new Dictionary<string, float>
{
["r"] = txt.color.r,
["g"] = txt.color.g,
["b"] = txt.color.b,
["a"] = txt.color.a
}
}).ToList()
},
["images"] = new Dictionary<string, object>
{
["count"] = images.Length,
["items"] = images.Take(5).Select(img => new Dictionary<string, object>
{
["name"] = img.name,
["sprite_name"] = img.sprite != null ? img.sprite.name : "(none)",
["color"] = new Dictionary<string, float>
{
["r"] = img.color.r,
["g"] = img.color.g,
["b"] = img.color.b,
["a"] = img.color.a
},
["raycast_target"] = img.raycastTarget,
["active"] = img.gameObject.activeInHierarchy
}).ToList()
},
["input_fields"] = inputFields.Length,
["sliders"] = sliders.Length,
["toggles"] = toggles.Length,
["scroll_views"] = scrollViews.Length
}
};
}).ToList(),
["event_system"] = eventSystem != null ? new Dictionary<string, object>
{
["name"] = eventSystem.name,
["current_selected"] = eventSystem.currentSelectedGameObject != null ? eventSystem.currentSelectedGameObject.name : null,
["send_navigation_events"] = eventSystem.sendNavigationEvents,
["pixel_drag_threshold"] = eventSystem.pixelDragThreshold
} : null
};
var settings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
};
return JsonConvert.SerializeObject(uiInfo, Formatting.Indented, settings);
}
catch (System.Exception e)
{
return $"Error getting UI information: {e.Message}";
}
#else
return "UI information is only available in Unity Editor";
#endif
}
/// <summary>
/// Get physics information
/// </summary>
public static string GetPhysicsInformation()
{
try
{
var rigidbodies = GameObject.FindObjectsOfType<Rigidbody>();
var colliders = GameObject.FindObjectsOfType<Collider>();
var joints = GameObject.FindObjectsOfType<Joint>();
var physicsInfo = new Dictionary<string, object>
{
["global_settings"] = new Dictionary<string, object>
{
["gravity"] = new Dictionary<string, float>
{
["x"] = Physics.gravity.x,
["y"] = Physics.gravity.y,
["z"] = Physics.gravity.z
},
["default_solver_iterations"] = Physics.defaultSolverIterations,
["default_solver_velocity_iterations"] = Physics.defaultSolverVelocityIterations,
["bounce_threshold"] = Physics.bounceThreshold,
["sleep_threshold"] = Physics.sleepThreshold,
["default_contact_offset"] = Physics.defaultContactOffset,
["queries_hit_triggers"] = Physics.queriesHitTriggers
},
["rigidbodies"] = new Dictionary<string, object>
{
["total_count"] = rigidbodies.Length,
["items"] = rigidbodies.Take(10).Select(rb => new Dictionary<string, object>
{
["name"] = rb.name,
["mass"] = rb.mass,
["drag"] = rb.linearDamping,
["angular_drag"] = rb.angularDamping,
["use_gravity"] = rb.useGravity,
["is_kinematic"] = rb.isKinematic,
["constraints"] = rb.constraints.ToString(),
["velocity"] = new Dictionary<string, object>
{
["magnitude"] = rb.linearVelocity.magnitude,
["vector"] = new Dictionary<string, float>
{
["x"] = rb.linearVelocity.x,
["y"] = rb.linearVelocity.y,
["z"] = rb.linearVelocity.z
}
},
["angular_velocity"] = new Dictionary<string, float>
{
["x"] = rb.angularVelocity.x,
["y"] = rb.angularVelocity.y,
["z"] = rb.angularVelocity.z
}
}).ToList()
},
["colliders"] = new Dictionary<string, object>
{
["total_count"] = colliders.Length,
["by_type"] = colliders.GroupBy(c => c.GetType().Name).Select(group => new Dictionary<string, object>
{
["type"] = group.Key,
["count"] = group.Count()
}).ToList(),
["triggers"] = colliders.Count(c => c.isTrigger),
["non_triggers"] = colliders.Count(c => !c.isTrigger)
},
["joints"] = new Dictionary<string, object>
{
["total_count"] = joints.Length,
["by_type"] = joints.GroupBy(j => j.GetType().Name).Select(group => new Dictionary<string, object>
{
["type"] = group.Key,
["count"] = group.Count()
}).ToList()
}
};
return JsonConvert.SerializeObject(physicsInfo, Formatting.Indented);
}
catch (System.Exception e)
{
return $"Error getting physics information: {e.Message}";
}
}
/// <summary>
/// Check implementation progress
/// </summary>
public static string GetImplementationProgress()
{
var info = new System.Text.StringBuilder();
info.AppendLine("📈 Implementation Progress Checklist");
// Check UI elements
var canvas = GameObject.FindObjectOfType<Canvas>();
info.AppendLine($"\n🖼️ UI Implementation:");
info.AppendLine($" ✓ Canvas: {(canvas != null ? "Present" : "None")}");
if (canvas != null)
{
var buttons = canvas.GetComponentsInChildren<UnityEngine.UI.Button>();
var texts = canvas.GetComponentsInChildren<UnityEngine.UI.Text>();
var images = canvas.GetComponentsInChildren<UnityEngine.UI.Image>();
info.AppendLine($" ✓ Buttons: {buttons.Length}");
info.AppendLine($" ✓ Texts: {texts.Length}");
info.AppendLine($" ✓ Images: {images.Length}");
}
// Check GameObjects
info.AppendLine($"\n🎮 GameObjects:");
var primitives = new[] { "Cube", "Sphere", "Cylinder", "Plane", "Capsule" };
foreach (var prim in primitives)
{
var count = GameObject.FindObjectsOfType<GameObject>()
.Count(go => go.name.Contains(prim));
if (count > 0)
{
info.AppendLine($" ✓ {prim}: {count}");
}
}
// Check script implementation
var customScripts = AssetDatabase.FindAssets("t:MonoScript", new[] { "Assets" })
.Select(g => AssetDatabase.GUIDToAssetPath(g))
.Where(p => !p.Contains("/Editor/") && !p.Contains("Nexus"))
.Select(p => Path.GetFileName(p))
.ToList();
info.AppendLine($"\n📝 Custom Scripts ({customScripts.Count}):");
foreach (var script in customScripts.Take(5))
{
info.AppendLine($" - {script}");
}
return info.ToString();
}
// Helper methods
private static string GetGameObjectHierarchy(GameObject obj, int depth, int maxDepth)
{
if (depth >= maxDepth) return "";
var indent = new string(' ', depth * 2);
var info = new System.Text.StringBuilder();
// Object name and components
info.Append($"{indent}├─ {obj.name}");
var components = obj.GetComponents<Component>()
.Where(c => c != null && !(c is Transform))
.Select(c => c.GetType().Name);
if (components.Any())
{
info.Append($" [{string.Join(", ", components)}]");
}
if (!obj.activeInHierarchy)
{
info.Append(" (Inactive)");
}
info.AppendLine();
// Child objects
foreach (Transform child in obj.transform)
{
info.Append(GetGameObjectHierarchy(child.gameObject, depth + 1, maxDepth));
}
return info.ToString();
}
private static string GetFullPath(GameObject obj)
{
var path = obj.name;
var parent = obj.transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 9322b6d5d3a284616bffe8a0ce6bc417
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusStateInspector.cs
uploadId: 920982
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 3ff7e139f4872445a94af71f9793b9a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusVFXBuilder.cs
uploadId: 920982
@@ -0,0 +1,73 @@
using System;
using System.IO;
using UnityEngine;
using SynapticAIPro;
namespace SynapticAIPro
{
/// <summary>
/// Centralized version management - reads from package.json
/// </summary>
public static class NexusVersion
{
private static string _cachedVersion = null;
private static string _packageJsonPath = null;
/// <summary>
/// Get the current version from package.json
/// </summary>
public static string Current
{
get
{
if (_cachedVersion == null)
{
_cachedVersion = ReadVersionFromPackageJson();
}
return _cachedVersion;
}
}
/// <summary>
/// Force refresh the cached version (useful after package update)
/// </summary>
public static void RefreshCache()
{
_cachedVersion = null;
}
private static string ReadVersionFromPackageJson()
{
try
{
if (_packageJsonPath == null)
{
_packageJsonPath = Path.Combine(Application.dataPath, "Synaptic AI Pro/package.json");
}
if (File.Exists(_packageJsonPath))
{
var json = File.ReadAllText(_packageJsonPath);
// Simple parsing - find "version": "x.x.x"
var versionIndex = json.IndexOf("\"version\"");
if (versionIndex >= 0)
{
var colonIndex = json.IndexOf(':', versionIndex);
var firstQuote = json.IndexOf('"', colonIndex);
var secondQuote = json.IndexOf('"', firstQuote + 1);
if (firstQuote >= 0 && secondQuote > firstQuote)
{
return json.Substring(firstQuote + 1, secondQuote - firstQuote - 1);
}
}
}
}
catch (Exception e)
{
SynLog.Warn($"[Synaptic] Failed to read version from package.json: {e.Message}");
}
return "1.0.0"; // Fallback
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 34dd79f80af8a469fb288808a529b914
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusVersion.cs
uploadId: 920982
@@ -0,0 +1,923 @@
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEditor;
using Newtonsoft.Json;
using System.Collections.Generic;
using SynapticAIPro;
namespace SynapticPro
{
/// <summary>
/// WebSocket client for editor mode
/// Manages communication with MCP server
/// </summary>
public class NexusWebSocketClient
{
private ClientWebSocket webSocket;
private CancellationTokenSource cancellationTokenSource;
private bool isConnected = false;
private Queue<string> messageQueue = new Queue<string>();
private bool shouldReconnect = true;
private int reconnectAttempts = 0;
private const int maxReconnectAttempts = 30;
private const int reconnectDelay = 2000; // 2 seconds between attempts
private string serverUrl = "ws://127.0.0.1:8090";
private const int CONNECT_TIMEOUT_SECONDS = 5;
private readonly List<Task> backgroundTasks = new List<Task>();
public bool IsConnected => isConnected;
public event Action<string> OnMessageReceived;
public event Action OnConnected;
public event Action OnDisconnected;
private static NexusWebSocketClient instance;
public static NexusWebSocketClient Instance
{
get
{
if (instance == null)
{
instance = new NexusWebSocketClient();
}
return instance;
}
}
private static void LogTaskFault(Task t, string label)
{
t.ContinueWith(
x => SynLog.Warn($"[Nexus WebSocket] {label} faulted: {x.Exception?.GetBaseException().Message}"),
TaskContinuationOptions.OnlyOnFaulted);
}
public async Task<bool> Connect(string url = "ws://127.0.0.1:8090")
{
shouldReconnect = true;
reconnectAttempts = 0;
while (shouldReconnect && reconnectAttempts < maxReconnectAttempts)
{
try
{
SynLog.Info($"[Nexus WebSocket] Connecting to {url}... (Attempt {reconnectAttempts + 1})");
webSocket = new ClientWebSocket();
cancellationTokenSource = new CancellationTokenSource();
using (var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token))
{
connectCts.CancelAfter(TimeSpan.FromSeconds(CONNECT_TIMEOUT_SECONDS));
await webSocket.ConnectAsync(new Uri(url), connectCts.Token);
}
isConnected = true;
reconnectAttempts = 0; // Reset on success
SynLog.Info("[Nexus WebSocket] Connected successfully!");
OnConnected?.Invoke();
// Start message receive loop (tracked so Disconnect can await)
var receiveTask = Task.Run(async () => await ReceiveLoop());
LogTaskFault(receiveTask, "ReceiveLoop");
backgroundTasks.Add(receiveTask);
// Start heartbeat
var heartbeatTask = Task.Run(async () => await HeartbeatLoop());
LogTaskFault(heartbeatTask, "HeartbeatLoop");
backgroundTasks.Add(heartbeatTask);
// Notify Unity is ready
await SendMessage(new { type = "unity_ready", version = NexusVersion.Current });
return true;
}
catch (Exception e)
{
Debug.LogError($"[Nexus WebSocket] Connection failed: {e.Message}");
isConnected = false;
reconnectAttempts++;
if (reconnectAttempts < maxReconnectAttempts && shouldReconnect)
{
SynLog.Info($"[Nexus WebSocket] Retrying in {reconnectDelay / 1000} seconds...");
await Task.Delay(reconnectDelay);
}
}
}
return false;
}
private async Task ReceiveLoop()
{
var buffer = new byte[4096];
var messageBuilder = new StringBuilder();
try
{
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer),
cancellationTokenSource.Token
);
if (result.MessageType == WebSocketMessageType.Text)
{
// Accumulate fragments until EndOfMessage. Without this,
// any message larger than 4096B (e.g. tool args / scene
// dumps) gets truncated mid-chunk and fails JSON parse —
// root cause of ESC-0102 (Win + Unity 6.3 MCP receive
// appears to "drop" large messages).
messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
if (result.EndOfMessage)
{
var message = messageBuilder.ToString();
messageBuilder.Clear();
SynLog.Info($"[Nexus WebSocket] Received ({message.Length} chars): {message.Substring(0, System.Math.Min(message.Length, 200))}");
lock (messageQueue)
{
messageQueue.Enqueue(message);
}
}
}
else if (result.MessageType == WebSocketMessageType.Close)
{
break;
}
}
}
catch (Exception e)
{
Debug.LogError($"[Nexus WebSocket] Receive error: {e.Message}");
}
finally
{
isConnected = false;
OnDisconnected?.Invoke();
}
}
public void ProcessMessages()
{
lock (messageQueue)
{
while (messageQueue.Count > 0)
{
var message = messageQueue.Dequeue();
OnMessageReceived?.Invoke(message);
try
{
var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(message);
if (data != null && data.ContainsKey("type"))
{
ProcessUnityCommand(data);
}
}
catch (Exception e)
{
Debug.LogError($"[Nexus WebSocket] Message processing error: {e.Message}");
}
}
}
}
public event Action<string, string> OnClaudeResponse; // sessionId, message
public event Action<string> OnChatStatusUpdate; // status message
private void ProcessUnityCommand(Dictionary<string, object> data)
{
var type = data["type"].ToString();
if (type == "unity_operation")
{
var command = data.ContainsKey("command") ? data["command"].ToString() : "";
var parameters = data.ContainsKey("parameters") ? data["parameters"] as Newtonsoft.Json.Linq.JObject : null;
SynLog.Info($"[Nexus WebSocket] Executing Unity command: {command}");
// Execute Unity operation in editor mode
EditorApplication.delayCall += () =>
{
ExecuteUnityOperation(command, parameters);
};
}
else if (type == "claude_response")
{
// Real-time response from Claude Desktop
var responseData = data.ContainsKey("data") ? data["data"] as Newtonsoft.Json.Linq.JObject : null;
if (responseData != null)
{
var message = responseData.Value<string>("message") ?? "";
var sessionId = responseData.Value<string>("sessionId") ?? "";
var responseType = responseData.Value<string>("responseType") ?? "response";
SynLog.Info($"[Nexus WebSocket] Claude response received: {message}");
// Fire event on main thread
EditorApplication.delayCall += () =>
{
OnClaudeResponse?.Invoke(sessionId, message);
};
}
}
else if (type == "chat_initiated")
{
// Chat initiated notification
var chatData = data.ContainsKey("data") ? data["data"] as Newtonsoft.Json.Linq.JObject : null;
if (chatData != null)
{
var status = chatData.Value<string>("status") ?? "Processing...";
SynLog.Info($"[Nexus WebSocket] Chat initiated: {status}");
EditorApplication.delayCall += () =>
{
OnChatStatusUpdate?.Invoke(status);
};
}
}
}
private async void ExecuteUnityOperation(string command, Newtonsoft.Json.Linq.JObject parameters)
{
SynLog.Info($"[Nexus WebSocket] Executing Unity operation: {command}");
SynLog.Info($"[Nexus WebSocket] Parameters: {parameters?.ToString()}");
var operationId = parameters?.Value<string>("operationId") ?? Guid.NewGuid().ToString();
try
{
var operation = new NexusUnityOperation
{
type = ConvertCommandToOperationType(command),
parameters = new Dictionary<string, string>()
};
// Convert parameters
if (parameters != null)
{
foreach (var prop in parameters.Properties())
{
if (prop.Name == "operationId") continue;
var value = prop.Value;
if (value is Newtonsoft.Json.Linq.JObject jObj)
{
// Process nested objects (Vector3, etc.)
if (jObj.ContainsKey("x") && jObj.ContainsKey("y") && jObj.ContainsKey("z"))
{
operation.parameters[prop.Name] = $"{jObj["x"]},{jObj["y"]},{jObj["z"]}";
}
else if (jObj.ContainsKey("x") && jObj.ContainsKey("y"))
{
operation.parameters[prop.Name] = $"{jObj["x"]},{jObj["y"]}";
}
else
{
operation.parameters[prop.Name] = value.ToString();
}
}
else
{
operation.parameters[prop.Name] = value.ToString();
}
}
}
// Execute
string result = "";
bool success = true;
// Process information retrieval commands
switch (operation.type)
{
case "GET_SCENE_INFO":
result = NexusStateInspector.GetSceneInformation();
break;
case "GET_CAMERA_INFO":
result = NexusStateInspector.GetCameraInformation();
break;
case "GET_TERRAIN_INFO":
result = NexusStateInspector.GetTerrainInformation();
break;
case "GET_LIGHTING_INFO":
result = NexusStateInspector.GetLightingInformation();
break;
case "GET_MATERIAL_INFO":
result = NexusStateInspector.GetMaterialInformation();
break;
case "GET_UI_INFO":
result = NexusStateInspector.GetUIInformation();
break;
case "GET_PHYSICS_INFO":
result = NexusStateInspector.GetPhysicsInformation();
break;
case "GET_GAMEOBJECT_DETAILS":
var name = operation.parameters.GetValueOrDefault("name", "");
result = NexusStateInspector.GetGameObjectDetails(name);
break;
case "GET_PROJECT_STATS":
result = NexusStateInspector.GetProjectStatistics();
break;
default:
// Normal operation.
// ESC-0107 fix: previously used `.Result` which blocks the
// main thread (delayCall) on a Task whose continuation may
// need to repost via UnitySynchronizationContext → classic
// SyncContext deadlock. ConfigureAwait(false) drops the
// captured context so the continuation can run on any
// thread; the body of ExecuteOperation is itself sync
// for most cases (e.g. RUN_CSHARP returns immediately).
var executor = new NexusUnityExecutor();
result = await executor.ExecuteOperation(operation).ConfigureAwait(false);
// Error check
if (result.StartsWith("Error") || result.Contains("not found") || result.Contains("failed"))
{
success = false;
}
break;
}
// 既存のMCP通信と同じフォーマットを使用
var response = new Dictionary<string, object>
{
["type"] = "operation_result",
["id"] = operationId,
["content"] = result,
["data"] = new Dictionary<string, object> { ["success"] = success }
};
SynLog.Info($"[Nexus WebSocket] Operation result: {result}");
// Send result to MCP server
await SendMessage(response);
}
catch (Exception e)
{
var errorResponse = new Dictionary<string, object>
{
["type"] = "operation_result",
["id"] = operationId,
["content"] = e.Message,
["data"] = new Dictionary<string, object> { ["success"] = false }
};
Debug.LogError($"[Nexus WebSocket] Operation execution error: {e.Message}\n{e.StackTrace}");
// Send error response to MCP server
await SendMessage(errorResponse);
}
}
private string ConvertCommandToOperationType(string command)
{
switch (command)
{
case "create_ui":
return "CREATE_UI";
case "create_gameobject":
return "CREATE_GAMEOBJECT";
case "instantiate_prefab":
return "INSTANTIATE_PREFAB";
case "set_transform":
return "SET_PROPERTY";
case "setup_camera":
return "SETUP_CAMERA";
case "create_particle_system":
return "CREATE_PARTICLE_SYSTEM";
case "setup_navmesh":
return "SETUP_NAVMESH";
case "create_audio_mixer":
return "CREATE_AUDIO_MIXER";
case "undo":
return "UNDO";
case "redo":
return "REDO";
case "get_history":
return "GET_HISTORY";
// Information retrieval
case "get_scene_info":
return "GET_SCENE_INFO";
case "get_camera_info":
return "GET_CAMERA_INFO";
case "get_terrain_info":
return "GET_TERRAIN_INFO";
case "get_lighting_info":
return "GET_LIGHTING_INFO";
case "get_material_info":
return "GET_MATERIAL_INFO";
case "get_ui_info":
return "GET_UI_INFO";
case "get_physics_info":
return "GET_PHYSICS_INFO";
case "get_gameobject_details":
return "GET_GAMEOBJECT_DETAILS";
case "list_assets":
return "LIST_ASSETS";
case "get_project_stats":
return "GET_PROJECT_STATS";
default:
return command.ToUpper();
}
}
public async Task SendMessage(object data)
{
if (!isConnected || webSocket.State != WebSocketState.Open)
{
SynLog.Warn("[Nexus WebSocket] Cannot send message: not connected");
return;
}
try
{
var json = JsonConvert.SerializeObject(data);
var bytes = Encoding.UTF8.GetBytes(json);
await webSocket.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
true,
cancellationTokenSource.Token
);
}
catch (Exception e)
{
Debug.LogError($"[Nexus WebSocket] Send error: {e.Message}");
}
}
private async Task HeartbeatLoop()
{
while (isConnected && !cancellationTokenSource.Token.IsCancellationRequested)
{
try
{
// Send heartbeat every 30 seconds
await Task.Delay(30000, cancellationTokenSource.Token);
if (webSocket.State == WebSocketState.Open)
{
await SendMessage(new { type = "heartbeat", timestamp = DateTime.Now.Ticks });
}
else
{
SynLog.Warn("[Nexus WebSocket] Connection lost during heartbeat");
isConnected = false;
OnDisconnected?.Invoke();
// Attempt reconnection
if (shouldReconnect)
{
var reconnectTask = Task.Run(async () => await Connect());
LogTaskFault(reconnectTask, "HeartbeatReconnect");
}
break;
}
}
catch (Exception e)
{
if (!cancellationTokenSource.Token.IsCancellationRequested)
{
Debug.LogError($"[Nexus WebSocket] Heartbeat error: {e.Message}");
}
break;
}
}
}
public async Task Disconnect()
{
shouldReconnect = false; // Disable auto-reconnect
if (webSocket != null && webSocket.State == WebSocketState.Open)
{
try
{
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Closing",
cancellationTokenSource.Token
);
}
catch (Exception e)
{
Debug.LogError($"[Nexus WebSocket] Disconnect error: {e.Message}");
}
}
cancellationTokenSource?.Cancel();
cancellationTokenSource?.Dispose();
isConnected = false;
// Give background tasks a bounded window to unwind before disposing resources.
if (backgroundTasks.Count > 0)
{
try { Task.WhenAll(backgroundTasks).Wait(TimeSpan.FromSeconds(2)); } catch { }
backgroundTasks.Clear();
}
}
/// <summary>
/// Set server URL
/// </summary>
public void SetServerUrl(string url)
{
serverUrl = url;
SynLog.Info($"[Nexus WebSocket] Server URL changed to: {url}");
}
}
/// <summary>
/// Manages WebSocket client updates
/// </summary>
[InitializeOnLoad]
public static class NexusWebSocketUpdater
{
static NexusWebSocketUpdater()
{
EditorApplication.update += Update;
}
private static void Update()
{
NexusWebSocketClient.Instance?.ProcessMessages();
NexusHTTPWebSocketClient.Instance?.ProcessMessages();
}
}
/// <summary>
/// WebSocket client for HTTP Server connection
/// Separate from MCP WebSocket to allow both to run simultaneously
/// </summary>
public class NexusHTTPWebSocketClient
{
private ClientWebSocket webSocket;
private CancellationTokenSource cancellationTokenSource;
private bool isConnected = false;
private Queue<string> messageQueue = new Queue<string>();
private bool shouldReconnect = true;
private int reconnectAttempts = 0;
private const int maxReconnectAttempts = 10;
private const int reconnectDelay = 1000;
private const int CONNECT_TIMEOUT_SECONDS = 5;
private readonly List<Task> backgroundTasks = new List<Task>();
// Reentrancy guard. Connect() can be invoked from three paths (UI button,
// NexusEditorMCPService HTTP auto-connect, and ReceiveLoop's finally auto-reconnect);
// without this gate, concurrent calls clobber `webSocket`/`cancellationTokenSource`
// and produce duplicate "Connecting/Connected/Disconnected" log spam.
private volatile bool connectInFlight = false;
private string serverUrl = "ws://127.0.0.1:8086";
private int port = 8086;
public bool IsConnected => isConnected;
public int Port => port;
private static NexusHTTPWebSocketClient instance;
public static NexusHTTPWebSocketClient Instance
{
get
{
if (instance == null)
{
instance = new NexusHTTPWebSocketClient();
}
return instance;
}
}
private static void LogTaskFault(Task t, string label)
{
t.ContinueWith(
x => SynLog.Warn($"[HTTP WebSocket] {label} faulted: {x.Exception?.GetBaseException().Message}"),
TaskContinuationOptions.OnlyOnFaulted);
}
public async Task<bool> Connect(int httpPort)
{
// Reentrancy guard — silently drop concurrent Connect calls.
if (connectInFlight)
{
return isConnected;
}
if (isConnected && port == httpPort)
{
return true;
}
connectInFlight = true;
try
{
port = httpPort;
serverUrl = $"ws://127.0.0.1:{port}";
shouldReconnect = true;
reconnectAttempts = 0;
while (shouldReconnect && reconnectAttempts < maxReconnectAttempts)
{
try
{
SynLog.Info($"[HTTP WebSocket] Connecting to {serverUrl}... (Attempt {reconnectAttempts + 1})");
var ws = new ClientWebSocket();
ws.Options.SetRequestHeader("X-Client-Type", "unity");
var cts = new CancellationTokenSource();
webSocket = ws;
cancellationTokenSource = cts;
using (var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token))
{
connectCts.CancelAfter(TimeSpan.FromSeconds(CONNECT_TIMEOUT_SECONDS));
await ws.ConnectAsync(new Uri(serverUrl), connectCts.Token);
}
isConnected = true;
reconnectAttempts = 0;
SynLog.Info($"[HTTP WebSocket] Connected to HTTP Server on port {port}!");
// Start message receive loop — snapshot ws+cts so a stale loop whose
// fields were overwritten by a subsequent Connect bails out cleanly.
var receiveTask = Task.Run(async () => await ReceiveLoop(ws, cts));
LogTaskFault(receiveTask, "ReceiveLoop");
backgroundTasks.Add(receiveTask);
// Notify Unity is ready
await SendMessage(new { type = "unity_ready", version = NexusVersion.Current });
return true;
}
catch (Exception e)
{
SynLog.Warn($"[HTTP WebSocket] Connection failed: {e.Message}");
isConnected = false;
reconnectAttempts++;
if (reconnectAttempts < maxReconnectAttempts && shouldReconnect)
{
await Task.Delay(reconnectDelay);
}
}
}
SynLog.Warn($"[HTTP WebSocket] Could not connect after {maxReconnectAttempts} attempts");
return false;
}
finally
{
connectInFlight = false;
}
}
private async Task ReceiveLoop(ClientWebSocket ws, CancellationTokenSource cts)
{
var buffer = new byte[8192];
var messageBuilder = new StringBuilder();
try
{
while (ws.State == WebSocketState.Open && !cts.IsCancellationRequested)
{
var result = await ws.ReceiveAsync(
new ArraySegment<byte>(buffer),
cts.Token
);
if (result.MessageType == WebSocketMessageType.Text)
{
messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
if (result.EndOfMessage)
{
var message = messageBuilder.ToString();
messageBuilder.Clear();
lock (messageQueue)
{
messageQueue.Enqueue(message);
}
}
}
else if (result.MessageType == WebSocketMessageType.Close)
{
break;
}
}
}
catch (Exception e)
{
if (!cts.IsCancellationRequested)
{
SynLog.Warn($"[HTTP WebSocket] Receive error: {e.Message}");
}
}
finally
{
// Only mutate shared state + auto-reconnect if this loop is STILL the active
// session. A later Connect() overwrites webSocket/cancellationTokenSource, so
// reference-equality on the snapshots tells us whether we're stale.
bool stillActive = ReferenceEquals(webSocket, ws)
&& ReferenceEquals(cancellationTokenSource, cts);
if (stillActive)
{
isConnected = false;
}
if (shouldReconnect
&& stillActive
&& !cts.IsCancellationRequested
&& !connectInFlight)
{
var reconnectTask = Task.Run(async () =>
{
await Task.Delay(reconnectDelay);
if (shouldReconnect && !connectInFlight)
await Connect(port);
});
LogTaskFault(reconnectTask, "AutoReconnect");
}
}
}
public void ProcessMessages()
{
if (!isConnected) return;
lock (messageQueue)
{
while (messageQueue.Count > 0)
{
var message = messageQueue.Dequeue();
try
{
var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(message);
if (data != null && data.ContainsKey("type"))
{
ProcessOperation(data);
}
}
catch (Exception e)
{
Debug.LogError($"[HTTP WebSocket] Message processing error: {e.Message}");
}
}
}
}
private void ProcessOperation(Dictionary<string, object> data)
{
var type = data["type"].ToString();
if (type == "operation")
{
var operationType = data.ContainsKey("operationType") ? data["operationType"].ToString() : "";
var operationId = data.ContainsKey("operationId") ? data["operationId"].ToString() : "";
var parameters = data.ContainsKey("parameters") ? data["parameters"] as Newtonsoft.Json.Linq.JObject : null;
// Execute immediately (ProcessMessages is called from main thread via EditorApplication.update)
_ = ExecuteOperation(operationType, operationId, parameters);
}
}
private async Task ExecuteOperation(string operationType, string operationId, Newtonsoft.Json.Linq.JObject parameters)
{
try
{
var operation = new NexusUnityOperation
{
type = operationType,
parameters = new Dictionary<string, string>()
};
// Convert parameters
if (parameters != null)
{
foreach (var prop in parameters.Properties())
{
var value = prop.Value;
if (value is Newtonsoft.Json.Linq.JObject jObj)
{
if (jObj.ContainsKey("x") && jObj.ContainsKey("y") && jObj.ContainsKey("z"))
{
operation.parameters[prop.Name] = $"{jObj["x"]},{jObj["y"]},{jObj["z"]}";
}
else
{
operation.parameters[prop.Name] = value.ToString();
}
}
else
{
operation.parameters[prop.Name] = value?.ToString() ?? "";
}
}
}
// Execute
var executor = new NexusUnityExecutor();
var result = await executor.ExecuteOperation(operation);
var success = !result.StartsWith("Error") && !result.Contains("failed");
// Send response
var response = new Dictionary<string, object>
{
["operationId"] = operationId,
["success"] = success,
["result"] = result
};
await SendMessage(response);
}
catch (Exception e)
{
var errorResponse = new Dictionary<string, object>
{
["operationId"] = operationId,
["success"] = false,
["result"] = $"Error: {e.Message}"
};
await SendMessage(errorResponse);
}
}
public async Task SendMessage(object data)
{
if (!isConnected || webSocket?.State != WebSocketState.Open)
{
return;
}
try
{
var json = JsonConvert.SerializeObject(data);
var bytes = Encoding.UTF8.GetBytes(json);
await webSocket.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
true,
cancellationTokenSource.Token
);
}
catch (Exception e)
{
Debug.LogError($"[HTTP WebSocket] Send error: {e.Message}");
}
}
public async Task Disconnect()
{
shouldReconnect = false;
isConnected = false;
try
{
if (webSocket != null && webSocket.State == WebSocketState.Open)
{
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)))
{
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Closing",
cts.Token
);
}
}
}
catch { }
try
{
cancellationTokenSource?.Cancel();
cancellationTokenSource?.Dispose();
}
catch { }
cancellationTokenSource = null;
webSocket = null;
if (backgroundTasks.Count > 0)
{
try { Task.WhenAll(backgroundTasks).Wait(TimeSpan.FromSeconds(2)); } catch { }
backgroundTasks.Clear();
}
SynLog.Info("[HTTP WebSocket] Disconnected");
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 42c5fbdad694f48c4a4eb6ca71c4e8af
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/NexusWebSocketClient.cs
uploadId: 920982
@@ -0,0 +1,223 @@
using UnityEngine;
using SynapticAIPro;
using UnityEditor;
using UnityEngine.Rendering;
using System.IO;
using System.Linq;
namespace Synaptic.Editor
{
/// <summary>
/// Automatically manages shader files based on the current render pipeline.
/// Disables shaders for pipelines that are not installed to prevent compilation errors.
/// </summary>
[InitializeOnLoad]
public static class ShaderPipelineManager
{
public enum RenderPipelineType
{
BuiltIn,
URP,
HDRP
}
private const string SHADER_BASE_PATH = "Assets/Synaptic AI Pro/Shaders";
private const string BUILTIN_FOLDER = "BuiltIn";
private const string URP_FOLDER = "URP";
private const string HDRP_FOLDER = "HDRP";
private const string DISABLED_EXTENSION = ".disabled";
private static readonly string[] PIPELINE_FOLDERS = { BUILTIN_FOLDER, URP_FOLDER, HDRP_FOLDER };
static ShaderPipelineManager()
{
// Delay execution to ensure Unity is fully initialized
EditorApplication.delayCall += OnEditorInitialized;
}
private static void OnEditorInitialized()
{
EditorApplication.delayCall -= OnEditorInitialized;
// Check and update shaders on editor load
UpdateShadersForCurrentPipeline();
// Subscribe to pipeline changes
RenderPipelineManager.activeRenderPipelineTypeChanged += OnPipelineChanged;
}
private static void OnPipelineChanged()
{
SynLog.Info("[Synaptic] Render pipeline changed, updating shaders...");
UpdateShadersForCurrentPipeline();
}
/// <summary>
/// Detects the currently active render pipeline.
/// </summary>
public static RenderPipelineType DetectCurrentPipeline()
{
var pipelineAsset = GraphicsSettings.currentRenderPipeline;
if (pipelineAsset == null)
{
return RenderPipelineType.BuiltIn;
}
var typeName = pipelineAsset.GetType().FullName;
if (typeName.Contains("Universal") || typeName.Contains("URP"))
{
return RenderPipelineType.URP;
}
if (typeName.Contains("HighDefinition") || typeName.Contains("HDRenderPipeline") || typeName.Contains("HDRP"))
{
return RenderPipelineType.HDRP;
}
return RenderPipelineType.BuiltIn;
}
/// <summary>
/// Updates shader files based on the current pipeline.
/// Enables shaders for the current pipeline and disables others.
/// </summary>
[MenuItem("Tools/Synaptic Pro/Update Shaders for Pipeline")]
public static void UpdateShadersForCurrentPipeline()
{
var currentPipeline = DetectCurrentPipeline();
SynLog.Info($"[Synaptic] Detected render pipeline: {currentPipeline}");
bool changed = false;
foreach (var folder in PIPELINE_FOLDERS)
{
string folderPath = Path.Combine(SHADER_BASE_PATH, folder);
if (!Directory.Exists(folderPath))
{
continue;
}
bool shouldEnable = ShouldEnableFolder(folder, currentPipeline);
changed |= UpdateShaderFolder(folderPath, shouldEnable);
}
if (changed)
{
AssetDatabase.Refresh();
SynLog.Info($"[Synaptic] Shaders updated for {currentPipeline} pipeline");
}
}
private static bool ShouldEnableFolder(string folder, RenderPipelineType currentPipeline)
{
switch (folder)
{
case BUILTIN_FOLDER:
return currentPipeline == RenderPipelineType.BuiltIn;
case URP_FOLDER:
return currentPipeline == RenderPipelineType.URP;
case HDRP_FOLDER:
return currentPipeline == RenderPipelineType.HDRP;
default:
return true;
}
}
private static bool UpdateShaderFolder(string folderPath, bool enable)
{
bool changed = false;
// Get all shader files (both enabled and disabled)
var shaderFiles = Directory.GetFiles(folderPath, "*.shader", SearchOption.AllDirectories)
.Concat(Directory.GetFiles(folderPath, "*.shader" + DISABLED_EXTENSION, SearchOption.AllDirectories))
.ToArray();
foreach (var filePath in shaderFiles)
{
bool isDisabled = filePath.EndsWith(DISABLED_EXTENSION);
if (enable && isDisabled)
{
// Enable: remove .disabled extension
string newPath = filePath.Substring(0, filePath.Length - DISABLED_EXTENSION.Length);
MoveFile(filePath, newPath);
changed = true;
}
else if (!enable && !isDisabled)
{
// Disable: add .disabled extension
string newPath = filePath + DISABLED_EXTENSION;
MoveFile(filePath, newPath);
changed = true;
}
}
return changed;
}
private static void MoveFile(string sourcePath, string destPath)
{
// Also handle .meta files
string sourceMetaPath = sourcePath + ".meta";
string destMetaPath = destPath + ".meta";
try
{
if (File.Exists(sourcePath))
{
File.Move(sourcePath, destPath);
}
if (File.Exists(sourceMetaPath))
{
File.Move(sourceMetaPath, destMetaPath);
}
}
catch (System.Exception e)
{
SynLog.Warn($"[Synaptic] Failed to move shader file: {e.Message}");
}
}
/// <summary>
/// Gets the appropriate shader name for the current pipeline.
/// </summary>
public static string GetShaderName(string baseShaderName)
{
var pipeline = DetectCurrentPipeline();
string pipelineFolder = GetPipelineFolderName(pipeline);
return $"Synaptic/{pipelineFolder}/{baseShaderName}";
}
/// <summary>
/// Gets the folder name for a specific pipeline type.
/// </summary>
public static string GetPipelineFolderName(RenderPipelineType pipeline)
{
switch (pipeline)
{
case RenderPipelineType.URP:
return URP_FOLDER;
case RenderPipelineType.HDRP:
return HDRP_FOLDER;
default:
return BUILTIN_FOLDER;
}
}
/// <summary>
/// Finds a shader compatible with the current pipeline.
/// Uses PackageRequirements-enabled multi-SubShader approach.
/// </summary>
public static Shader FindShaderForCurrentPipeline(string baseShaderName)
{
// Shaders now use PackageRequirements tags, so Unity automatically
// skips SubShaders for missing render pipelines.
// Just use the standard shader name.
return Shader.Find($"Synaptic/{baseShaderName}");
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 37c91c2c6cc704a91acff688d62c58e9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/ShaderPipelineManager.cs
uploadId: 920982
+72
View File
@@ -0,0 +1,72 @@
using UnityEngine;
using UnityEditor;
namespace SynapticAIPro
{
/// <summary>
/// Synaptic AI Pro 内部ログのラッパー。
/// EditorPrefs "Synaptic.VerboseLog" で Info/Warn の出力を抑制可能。
/// Error は常に出力される(重要なエラーは隠さない)。
/// </summary>
public static class SynLog
{
private const string PREF_KEY = "Synaptic.VerboseLog";
// EditorPrefs.GetBool is main-thread only. Calling it from background
// threads (e.g. WebSocket ReceiveAsync handlers) throws and silently
// kills the calling Task. We snapshot the value into a volatile bool
// on the main thread, and Info/Warn read the cache (thread-safe).
// This was the root cause of v1.2.20/v1.2.21 MCP timeout (ESC-0102).
private static volatile bool _verboseCached = true;
private static bool _initialized = false;
[InitializeOnLoadMethod]
private static void InitVerboseCache()
{
try { _verboseCached = EditorPrefs.GetBool(PREF_KEY, true); }
catch { _verboseCached = true; }
_initialized = true;
}
public static bool VerboseEnabled
{
get => _initialized ? _verboseCached : true;
set
{
_verboseCached = value;
try { EditorPrefs.SetBool(PREF_KEY, value); } catch { }
}
}
public static void Info(string msg)
{
if (_verboseCached) Debug.Log(msg);
}
public static void Info(string msg, Object context)
{
if (_verboseCached) Debug.Log(msg, context);
}
public static void Warn(string msg)
{
if (_verboseCached) Debug.LogWarning(msg);
}
public static void Warn(string msg, Object context)
{
if (_verboseCached) Debug.LogWarning(msg, context);
}
// Error は常に出力(重要な情報)
public static void Error(string msg)
{
Debug.LogError(msg);
}
public static void Error(string msg, Object context)
{
Debug.LogError(msg, context);
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 531ccf9e9ffa64e5b921f423f2af6fd6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/SynLog.cs
uploadId: 920982
@@ -0,0 +1,185 @@
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
namespace SynapticPro
{
/// <summary>
/// Launches Node.js HTTP server detached from Unity's Win32 JobObject.
///
/// Unity Editor on Windows assigns a Job Object with
/// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE to itself; any child process started
/// via Process.Start inherits the Job and is killed when Unity manipulates
/// the Job (assembly reload, PlayMode transitions, certain GC paths).
///
/// This launcher uses CreateProcessW via P/Invoke with
/// CREATE_BREAKAWAY_FROM_JOB so the spawned node.exe is independent of
/// Unity's Job. Combined with a parent-PID watchdog in http-server.js the
/// child is reliably tied to Unity's lifecycle without being subject to
/// Job-Object cascade kills.
///
/// PID is persisted in SessionState so the same process can be re-attached
/// after domain reload (recovers ESC-0095 "Connect Unity Only" case).
/// </summary>
public static class SynapticDetachedProcess
{
public const string PID_KEY = "Synaptic.NodeServer.PID";
public const string PORT_KEY = "Synaptic.NodeServer.PORT";
// ----- Win32 P/Invoke -----
private const uint CREATE_BREAKAWAY_FROM_JOB = 0x01000000;
private const uint CREATE_NEW_PROCESS_GROUP = 0x00000200;
private const uint DETACHED_PROCESS = 0x00000008;
private const uint CREATE_NO_WINDOW = 0x08000000;
private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute;
public int dwFlags;
public short wShowWindow, cbReserved2;
public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess, hThread;
public int dwProcessId, dwThreadId;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CreateProcessW(
string lpApplicationName, string lpCommandLine,
IntPtr lpProcessAttributes, IntPtr lpThreadAttributes,
bool bInheritHandles, uint dwCreationFlags,
IntPtr lpEnvironment, string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr h);
/// <summary>
/// Start node.exe http-server.js detached from Unity's Job.
/// Returns spawned PID on success, 0 on failure.
/// Windows only — caller falls back to Process.Start on other platforms.
/// </summary>
public static int StartWindows(string nodeExe, string scriptPath, int port, string mcpServerDir, string logFile)
{
int unityPid = Process.GetCurrentProcess().Id;
string cmd = $"\"{nodeExe}\" \"{scriptPath}\" {port} --parent-pid={unityPid} --log=\"{logFile}\"";
var si = new STARTUPINFO { cb = Marshal.SizeOf<STARTUPINFO>() };
uint flags = CREATE_BREAKAWAY_FROM_JOB
| DETACHED_PROCESS
| CREATE_NEW_PROCESS_GROUP
| CREATE_NO_WINDOW
| CREATE_UNICODE_ENVIRONMENT;
bool ok = CreateProcessW(
null, cmd,
IntPtr.Zero, IntPtr.Zero,
false, flags,
IntPtr.Zero, mcpServerDir,
ref si, out PROCESS_INFORMATION pi);
// Fallback: if BREAKAWAY denied (ACCESS_DENIED on JobObject without
// JOB_OBJECT_LIMIT_BREAKAWAY_OK), retry with DETACHED only and rely
// on node-side parent-PID watchdog for orphan cleanup.
if (!ok)
{
int err = Marshal.GetLastWin32Error();
UnityEngine.Debug.LogWarning($"[Synaptic] CreateProcess with BREAKAWAY failed (err={err}). Retrying without BREAKAWAY.");
flags &= ~CREATE_BREAKAWAY_FROM_JOB;
ok = CreateProcessW(
null, cmd,
IntPtr.Zero, IntPtr.Zero,
false, flags,
IntPtr.Zero, mcpServerDir,
ref si, out pi);
if (!ok)
{
int err2 = Marshal.GetLastWin32Error();
UnityEngine.Debug.LogError($"[Synaptic] CreateProcess fallback also failed (err={err2}).");
return 0;
}
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
SessionState.SetInt(PID_KEY, pi.dwProcessId);
SessionState.SetInt(PORT_KEY, port);
EditorPrefs.SetInt(PID_KEY, pi.dwProcessId);
EditorPrefs.SetInt(PORT_KEY, port);
return pi.dwProcessId;
}
/// <summary>
/// Check whether the previously-spawned PID is still alive.
/// Used after domain reload to recover the connection rather than
/// re-spawning.
/// </summary>
public static bool IsStoredProcessAlive(out int pid, out int port)
{
pid = SessionState.GetInt(PID_KEY, 0);
port = SessionState.GetInt(PORT_KEY, 0);
if (pid == 0) { pid = EditorPrefs.GetInt(PID_KEY, 0); port = EditorPrefs.GetInt(PORT_KEY, 0); }
if (pid == 0) return false;
try
{
var p = Process.GetProcessById(pid);
if (p == null || p.HasExited) return false;
// Sanity: must be a node process
string name = p.ProcessName.ToLowerInvariant();
return name.Contains("node");
}
catch
{
return false;
}
}
public static void ClearStoredPid()
{
SessionState.SetInt(PID_KEY, 0);
SessionState.SetInt(PORT_KEY, 0);
EditorPrefs.DeleteKey(PID_KEY);
EditorPrefs.DeleteKey(PORT_KEY);
}
public static bool KillStored()
{
int pid = SessionState.GetInt(PID_KEY, 0);
if (pid == 0) pid = EditorPrefs.GetInt(PID_KEY, 0);
if (pid == 0) return false;
try
{
var p = Process.GetProcessById(pid);
if (!p.HasExited)
{
p.Kill();
p.WaitForExit(3000);
}
return true;
}
catch
{
return false;
}
finally
{
ClearStoredPid();
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: d417153078dbb4acd965a32601264b9e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/SynapticDetachedProcess.cs
uploadId: 920982
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fcd8fd59bc5d340b0a82979773cfe49d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
{
"name": "Synaptic.MCP.Unity.TestRunner",
"rootNamespace": "SynapticPro.TestRunner",
"references": [
"Synaptic.MCP.Unity.Editor",
"Unity.Nuget.Newtonsoft-Json",
"UnityEditor.TestRunner",
"UnityEngine.TestRunner"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 918ef77181e7a491d91f95e03029ffdf
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/TestRunner/NexusAI.TestRunner.asmdef
uploadId: 920982
@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.TestTools.TestRunner.Api;
using Newtonsoft.Json;
namespace SynapticPro.TestRunner
{
/// <summary>
/// TestRunner service that can be called from NexusExecutor via reflection.
/// This class is only compiled when UNITY_INCLUDE_TESTS is defined.
/// </summary>
public static class NexusTestRunnerService
{
// Static fields to store test execution state
private static bool _isTestRunning = false;
private static List<TestResultInfo> _testResults = new List<TestResultInfo>();
private static int _totalTests = 0;
private static int _completedTests = 0;
private static string _currentTestMode = "";
private static TestRunnerApi _testRunnerApi;
private class TestResultInfo
{
public string name;
public string status;
public double duration;
public string message;
}
/// <summary>
/// Main entry point - called from NexusExecutor via reflection
/// </summary>
public static string Execute(string operation, string mode, string filter)
{
try
{
switch (operation.ToLower())
{
case "run":
return RunTestsWithApi(mode, filter);
case "results":
return GetTestResults();
case "list":
return ListAvailableTests(mode);
default:
return JsonConvert.SerializeObject(new
{
success = false,
error = $"Unknown operation: {operation}. Use 'run', 'results', or 'list'."
});
}
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new { success = false, error = e.Message });
}
}
private static string RunTestsWithApi(string testMode, string filter)
{
if (_isTestRunning)
{
return JsonConvert.SerializeObject(new
{
success = true,
status = "running",
mode = _currentTestMode,
totalTests = _totalTests,
completedTests = _completedTests,
message = "Tests are still running. Use operation='results' to check progress."
});
}
// Reset state
_isTestRunning = true;
_testResults.Clear();
_totalTests = 0;
_completedTests = 0;
_currentTestMode = testMode;
// Create TestRunnerApi instance
_testRunnerApi = ScriptableObject.CreateInstance<TestRunnerApi>();
// Register callbacks
_testRunnerApi.RegisterCallbacks(new SynapticTestCallbacks());
// Build filter
var testModeEnum = testMode.ToLower() == "playmode"
? TestMode.PlayMode
: TestMode.EditMode;
var executionSettings = new ExecutionSettings
{
filters = new[] { new Filter { testMode = testModeEnum } }
};
if (!string.IsNullOrEmpty(filter))
{
executionSettings.filters[0].testNames = new[] { filter };
}
// Start execution
_testRunnerApi.Execute(executionSettings);
return JsonConvert.SerializeObject(new
{
success = true,
status = "started",
mode = testMode,
message = "Tests started. Use operation='results' to check progress and get results."
});
}
private static string GetTestResults()
{
return JsonConvert.SerializeObject(new
{
success = true,
isRunning = _isTestRunning,
mode = _currentTestMode,
totalTests = _totalTests,
completedTests = _completedTests,
results = _testResults.Select(r => new
{
name = r.name,
status = r.status,
duration = r.duration,
message = r.message
}).ToList(),
summary = new
{
passed = _testResults.Count(r => r.status == "Passed"),
failed = _testResults.Count(r => r.status == "Failed"),
skipped = _testResults.Count(r => r.status == "Skipped")
}
}, Formatting.Indented);
}
private static string ListAvailableTests(string testMode)
{
var api = ScriptableObject.CreateInstance<TestRunnerApi>();
var testModeEnum = testMode.ToLower() == "playmode"
? TestMode.PlayMode
: TestMode.EditMode;
var tests = new List<string>();
api.RetrieveTestList(testModeEnum, (testRoot) =>
{
CollectTestNames(testRoot, tests);
});
return JsonConvert.SerializeObject(new
{
success = true,
mode = testMode,
testCount = tests.Count,
tests = tests
}, Formatting.Indented);
}
private static void CollectTestNames(ITestAdaptor test, List<string> tests)
{
if (test == null) return;
if (test.IsSuite)
{
foreach (var child in test.Children)
{
CollectTestNames(child, tests);
}
}
else
{
tests.Add(test.FullName);
}
}
private class SynapticTestCallbacks : ICallbacks
{
public void RunStarted(ITestAdaptor testsToRun)
{
_totalTests = CountTests(testsToRun);
Debug.Log($"[Synaptic] Test run started. Total tests: {_totalTests}");
}
public void RunFinished(ITestResultAdaptor result)
{
_isTestRunning = false;
Debug.Log($"[Synaptic] Test run finished. Passed: {_testResults.Count(r => r.status == "Passed")}, Failed: {_testResults.Count(r => r.status == "Failed")}");
}
public void TestStarted(ITestAdaptor test)
{
// Optional: Log test start
}
public void TestFinished(ITestResultAdaptor result)
{
_completedTests++;
var testResult = new TestResultInfo
{
name = result.Test.FullName,
status = result.TestStatus.ToString(),
duration = result.Duration,
message = result.Message ?? ""
};
_testResults.Add(testResult);
}
private int CountTests(ITestAdaptor test)
{
if (test == null) return 0;
if (test.IsSuite)
{
int count = 0;
foreach (var child in test.Children)
{
count += CountTests(child);
}
return count;
}
return 1;
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: f9fcb2cb1828f43ac8ccb7e67814d504
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 336030
packageName: Synaptic AI Pro - Natural Language Control for Unity
packageVersion: 1.2.23
assetPath: Assets/Synaptic AI Pro/Editor/TestRunner/NexusTestRunnerService.cs
uploadId: 920982