97ac0f71f5
https://assetstore.unity.com/packages/tools/generative-ai/synaptic-ai-pro-natural-language-control-for-unity-336030
421 lines
16 KiB
C#
421 lines
16 KiB
C#
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");
|
|
}
|
|
}
|
|
}
|