Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:19:28 +08:00
commit 1da7b24c8e
254 changed files with 43797 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditorToolkit.Protocol;
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Base handler for JSON-RPC commands
/// </summary>
public abstract class BaseHandler
{
/// <summary>
/// GameObject 캐시 (WeakReference 사용하여 메모리 누수 방지)
/// </summary>
private static Dictionary<string, System.WeakReference> gameObjectCache = new Dictionary<string, System.WeakReference>();
private static readonly object cacheLock = new object();
/// <summary>
/// Handler category (e.g., "GameObject", "Transform")
/// </summary>
public abstract string Category { get; }
/// <summary>
/// Handle JSON-RPC request
/// </summary>
/// <param name="request">JSON-RPC request</param>
/// <returns>Response object or null for error</returns>
public object Handle(JsonRpcRequest request)
{
try
{
// Validate request
if (request == null)
{
throw new ArgumentNullException(nameof(request), "Request cannot be null");
}
// Validate method name
string fullMethod = request.Method;
if (string.IsNullOrWhiteSpace(fullMethod))
{
throw new ArgumentException("Method name cannot be null or empty", nameof(request.Method));
}
// Validate method belongs to this handler category
if (!fullMethod.StartsWith(Category + "."))
{
throw new ArgumentException($"Invalid method for {Category} handler: {fullMethod}");
}
string methodName = fullMethod.Substring(Category.Length + 1);
// Validate extracted method name
if (string.IsNullOrWhiteSpace(methodName))
{
throw new ArgumentException($"Method name is empty after removing category prefix: {fullMethod}");
}
// Route to specific handler method
return HandleMethod(methodName, request);
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[{Category}] Handler error: {ex.Message}\n{ex.StackTrace}");
throw;
}
}
/// <summary>
/// Handle specific method (must be implemented by subclass)
/// </summary>
protected abstract object HandleMethod(string method, JsonRpcRequest request);
/// <summary>
/// Validate required parameter
/// </summary>
protected T ValidateParam<T>(JsonRpcRequest request, string paramName) where T : class
{
var paramsObj = request.GetParams<T>();
if (paramsObj == null)
{
throw new Exception($"Missing or invalid parameter: {paramName}");
}
return paramsObj;
}
/// <summary>
/// Find GameObject by name or path (캐싱 적용)
/// </summary>
public UnityEngine.GameObject FindGameObject(string name)
{
if (string.IsNullOrEmpty(name))
{
return null;
}
// 캐시 확인
lock (cacheLock)
{
if (gameObjectCache.TryGetValue(name, out var weakRef) && weakRef.IsAlive)
{
var cachedObj = weakRef.Target as UnityEngine.GameObject;
if (cachedObj != null && cachedObj.scene.IsValid())
{
return cachedObj;
}
else
{
// 캐시 무효화 (객체가 파괴됨)
gameObjectCache.Remove(name);
}
}
}
// Try direct find first (빠른 검색)
var obj = UnityEngine.GameObject.Find(name);
if (obj != null)
{
CacheGameObject(name, obj);
return obj;
}
// Try finding in all objects (including inactive) - 비용이 큼
var allObjects = UnityEngine.Resources.FindObjectsOfTypeAll<UnityEngine.GameObject>();
foreach (var go in allObjects)
{
if (go.name == name || GetGameObjectPath(go) == name)
{
// Make sure it's a scene object, not asset
if (go.scene.IsValid())
{
CacheGameObject(name, go);
return go;
}
}
}
return null;
}
/// <summary>
/// GameObject를 캐시에 추가
/// </summary>
private void CacheGameObject(string name, UnityEngine.GameObject obj)
{
lock (cacheLock)
{
gameObjectCache[name] = new System.WeakReference(obj);
// 캐시 크기 제한 (최대 100개)
if (gameObjectCache.Count > 100)
{
// 만료된(파괴된) 캐시 항목 제거
var toRemove = new List<string>();
foreach (var kvp in gameObjectCache)
{
if (!kvp.Value.IsAlive)
{
toRemove.Add(kvp.Key);
}
}
if (toRemove.Count > 0)
{
foreach (var key in toRemove)
{
gameObjectCache.Remove(key);
}
}
// 여전히 캐시 크기가 100개를 초과하면, 일부 항목을 제거하여 공간 확보
while (gameObjectCache.Count > 100)
{
// 가장 간단한 방법으로 첫 번째 항목 제거
// 더 나은 방법은 LRU(Least Recently Used) 정책을 구현하는 것입니다
var keyToRemove = gameObjectCache.Keys.First();
gameObjectCache.Remove(keyToRemove);
}
}
}
}
/// <summary>
/// 캐시 비우기 (테스트용 또는 메모리 정리)
/// </summary>
public static void ClearCache()
{
lock (cacheLock)
{
gameObjectCache.Clear();
}
}
/// <summary>
/// Get full path of GameObject in hierarchy
/// </summary>
protected string GetGameObjectPath(UnityEngine.GameObject obj)
{
string path = obj.name;
var parent = obj.transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9d527c88236424740b94eab71b673607

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEditorToolkit.Protocol;
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Console commands
/// </summary>
public class ConsoleHandler : BaseHandler
{
public override string Category => "Console";
// Store console logs (Queue로 변경 - O(1) 삽입/삭제)
private static Queue<ConsoleLogEntry> logEntries = new Queue<ConsoleLogEntry>(1000);
private static readonly object logLock = new object();
private static bool isListening = false;
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "GetLogs":
return HandleGetLogs(request);
case "Clear":
return HandleClear(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
public static void StartListening()
{
if (isListening) return;
Application.logMessageReceived += OnLogMessageReceived;
isListening = true;
}
public static void StopListening()
{
if (!isListening) return;
Application.logMessageReceived -= OnLogMessageReceived;
isListening = false;
}
private static void OnLogMessageReceived(string message, string stackTrace, LogType type)
{
lock (logLock)
{
logEntries.Enqueue(new ConsoleLogEntry
{
message = message,
stackTrace = stackTrace,
type = (int)type,
timestamp = DateTime.Now.ToString("HH:mm:ss.fff")
});
// Keep only last 1000 logs (✅ O(1) 연산으로 최적화)
if (logEntries.Count > 1000)
{
logEntries.Dequeue();
}
}
}
private object HandleGetLogs(JsonRpcRequest request)
{
var param = request.GetParams<GetLogsParams>() ?? new GetLogsParams { count = 50 };
lock (logLock)
{
var logs = new List<ConsoleLogEntry>();
// Queue를 Array로 변환하여 인덱스 접근
var logArray = logEntries.ToArray();
int start = Math.Max(0, logArray.Length - param.count);
for (int i = start; i < logArray.Length; i++)
{
var log = logArray[i];
// Filter by type
if (param.errorsOnly)
{
if (log.type != (int)LogType.Error && log.type != (int)LogType.Exception)
continue;
}
else if (!param.includeWarnings)
{
if (log.type == (int)LogType.Warning)
continue;
}
logs.Add(log);
}
return logs;
}
}
private object HandleClear(JsonRpcRequest request)
{
lock (logLock)
{
logEntries.Clear();
}
#if UNITY_EDITOR
// Also clear Unity Editor console (✅ Reflection null 체크 추가)
try
{
var assembly = Assembly.GetAssembly(typeof(UnityEditor.Editor));
if (assembly != null)
{
var type = assembly.GetType("UnityEditor.LogEntries");
if (type != null)
{
var method = type.GetMethod("Clear");
if (method != null)
{
method.Invoke(null, null);
}
else
{
Debug.LogWarning("LogEntries.Clear method not found");
}
}
else
{
Debug.LogWarning("UnityEditor.LogEntries type not found");
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to clear Editor console: {ex.Message}");
}
#endif
return new { success = true };
}
// Parameter classes
[Serializable]
public class GetLogsParams
{
public int count = 50;
public bool errorsOnly = false;
public bool includeWarnings = false;
}
[Serializable]
public class ConsoleLogEntry
{
public string message;
public string stackTrace;
public int type; // LogType: Error=0, Assert=1, Warning=2, Log=3, Exception=4
public string timestamp;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cce460e9a41c1244180b19652fd2de16

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditorToolkit.Protocol;
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for GameObject commands
/// </summary>
public class GameObjectHandler : BaseHandler
{
public override string Category => "GameObject";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Find":
return HandleFind(request);
case "Create":
return HandleCreate(request);
case "Destroy":
return HandleDestroy(request);
case "SetActive":
return HandleSetActive(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
/// <summary>
/// Find GameObject by name or path
/// </summary>
private object HandleFind(JsonRpcRequest request)
{
var param = ValidateParam<FindParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
return new GameObjectInfo
{
name = obj.name,
instanceId = obj.GetInstanceID(),
path = GetGameObjectPath(obj),
active = obj.activeSelf,
tag = obj.tag,
layer = obj.layer
};
}
/// <summary>
/// Create new GameObject
/// </summary>
private object HandleCreate(JsonRpcRequest request)
{
var param = ValidateParam<CreateParams>(request, "name");
GameObject obj = new GameObject(param.name);
// Set parent if specified
if (!string.IsNullOrEmpty(param.parent))
{
var parentObj = FindGameObject(param.parent);
if (parentObj == null)
{
GameObject.DestroyImmediate(obj);
throw new Exception($"Parent GameObject not found: {param.parent}");
}
obj.transform.SetParent(parentObj.transform);
}
// Register undo
#if UNITY_EDITOR
UnityEditor.Undo.RegisterCreatedObjectUndo(obj, "Create GameObject");
#endif
return new GameObjectInfo
{
name = obj.name,
instanceId = obj.GetInstanceID(),
path = GetGameObjectPath(obj),
active = obj.activeSelf,
tag = obj.tag,
layer = obj.layer
};
}
/// <summary>
/// Destroy GameObject
/// </summary>
private object HandleDestroy(JsonRpcRequest request)
{
var param = ValidateParam<FindParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
#if UNITY_EDITOR
UnityEditor.Undo.DestroyObjectImmediate(obj);
#else
GameObject.DestroyImmediate(obj);
#endif
return new { success = true };
}
/// <summary>
/// Set GameObject active state
/// </summary>
private object HandleSetActive(JsonRpcRequest request)
{
var param = ValidateParam<SetActiveParams>(request, "name and active");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
#if UNITY_EDITOR
// ✅ RegisterCompleteObjectUndo 사용 (GameObject 전체 상태 기록)
UnityEditor.Undo.RegisterCompleteObjectUndo(obj, "Set Active");
#endif
obj.SetActive(param.active);
return new { success = true, active = obj.activeSelf };
}
// Parameter classes (✅ private으로 변경)
[Serializable]
private class FindParams
{
public string name;
}
[Serializable]
private class CreateParams
{
public string name;
public string parent;
}
[Serializable]
private class SetActiveParams
{
public string name;
public bool active;
}
// Response classes
[Serializable]
public class GameObjectInfo
{
public string name;
public int instanceId;
public string path;
public bool active;
public string tag;
public int layer;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2545c55dd409a984a8bde59ac3f6ea5c

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditorToolkit.Protocol;
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Hierarchy commands
/// </summary>
public class HierarchyHandler : BaseHandler
{
public override string Category => "Hierarchy";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Get":
return HandleGet(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
private object HandleGet(JsonRpcRequest request)
{
var param = request.GetParams<GetParams>() ?? new GetParams();
var rootObjects = new List<GameObjectInfo>();
var scene = SceneManager.GetActiveScene();
if (!scene.IsValid())
{
return rootObjects;
}
foreach (var rootGO in scene.GetRootGameObjects())
{
// Skip inactive if requested
if (!param.includeInactive && !rootGO.activeSelf)
continue;
var info = BuildGameObjectInfo(rootGO, !param.rootOnly, param.includeInactive);
rootObjects.Add(info);
}
return rootObjects;
}
private GameObjectInfo BuildGameObjectInfo(GameObject obj, bool includeChildren, bool includeInactive)
{
var info = new GameObjectInfo
{
name = obj.name,
instanceId = obj.GetInstanceID(),
path = GetGameObjectPath(obj),
active = obj.activeSelf,
tag = obj.tag,
layer = obj.layer
};
if (includeChildren && obj.transform.childCount > 0)
{
info.children = new List<GameObjectInfo>();
for (int i = 0; i < obj.transform.childCount; i++)
{
var child = obj.transform.GetChild(i).gameObject;
// Skip inactive if requested
if (!includeInactive && !child.activeSelf)
continue;
var childInfo = BuildGameObjectInfo(child, true, includeInactive);
info.children.Add(childInfo);
}
}
return info;
}
// Parameter classes
[Serializable]
public class GetParams
{
public bool rootOnly = false;
public bool includeInactive = false;
}
[Serializable]
public class GameObjectInfo
{
public string name;
public int instanceId;
public string path;
public bool active;
public string tag;
public int layer;
public List<GameObjectInfo> children;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 021a56e56e85019408bdd9dd044b87e7

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditorToolkit.Protocol;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Scene commands
/// </summary>
public class SceneHandler : BaseHandler
{
public override string Category => "Scene";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "GetCurrent":
return HandleGetCurrent(request);
case "GetAll":
return HandleGetAll(request);
case "Load":
return HandleLoad(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
private object HandleGetCurrent(JsonRpcRequest request)
{
var scene = SceneManager.GetActiveScene();
return GetSceneInfo(scene);
}
private object HandleGetAll(JsonRpcRequest request)
{
var scenes = new List<SceneInfo>();
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
scenes.Add(GetSceneInfo(scene));
}
return scenes;
}
private object HandleLoad(JsonRpcRequest request)
{
var param = ValidateParam<LoadParams>(request, "name");
#if UNITY_EDITOR
// Editor mode: Use EditorSceneManager for proper undo/redo
try
{
var mode = param.additive ? OpenSceneMode.Additive : OpenSceneMode.Single;
EditorSceneManager.OpenScene(param.name, mode);
return new { success = true };
}
catch (Exception ex)
{
throw new Exception($"Failed to load scene: {ex.Message}");
}
#else
// Runtime mode: Use SceneManager
try
{
var mode = param.additive ? LoadSceneMode.Additive : LoadSceneMode.Single;
SceneManager.LoadScene(param.name, mode);
return new { success = true };
}
catch (Exception ex)
{
throw new Exception($"Failed to load scene: {ex.Message}");
}
#endif
}
private SceneInfo GetSceneInfo(Scene scene)
{
return new SceneInfo
{
name = scene.name,
path = scene.path,
buildIndex = scene.buildIndex,
isLoaded = scene.isLoaded,
isDirty = scene.isDirty,
rootCount = scene.rootCount
};
}
// Parameter classes
[Serializable]
public class LoadParams
{
public string name;
public bool additive;
}
[Serializable]
public class SceneInfo
{
public string name;
public string path;
public int buildIndex;
public bool isLoaded;
public bool isDirty;
public int rootCount;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2273c4afb7092144b92bbc657dbae285

View File

@@ -0,0 +1,197 @@
using System;
using UnityEngine;
using UnityEditorToolkit.Protocol;
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Transform commands
/// </summary>
public class TransformHandler : BaseHandler
{
public override string Category => "Transform";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "GetPosition":
return HandleGetPosition(request);
case "SetPosition":
return HandleSetPosition(request);
case "GetRotation":
return HandleGetRotation(request);
case "SetRotation":
return HandleSetRotation(request);
case "GetScale":
return HandleGetScale(request);
case "SetScale":
return HandleSetScale(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
private object HandleGetPosition(JsonRpcRequest request)
{
var param = ValidateParam<NameParam>(request, "name");
var transform = GetTransform(param.name);
return new Vector3Data
{
x = transform.position.x,
y = transform.position.y,
z = transform.position.z
};
}
private object HandleSetPosition(JsonRpcRequest request)
{
var param = ValidateParam<SetPositionParam>(request, "name and position");
var transform = GetTransform(param.name);
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(transform, "Set Position");
#endif
// ✅ ToVector3() 사용 (유효성 검증 포함)
transform.position = param.position.ToVector3();
return new { success = true };
}
private object HandleGetRotation(JsonRpcRequest request)
{
var param = ValidateParam<NameParam>(request, "name");
var transform = GetTransform(param.name);
var euler = transform.eulerAngles;
return new Vector3Data
{
x = euler.x,
y = euler.y,
z = euler.z
};
}
private object HandleSetRotation(JsonRpcRequest request)
{
var param = ValidateParam<SetRotationParam>(request, "name and rotation");
var transform = GetTransform(param.name);
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(transform, "Set Rotation");
#endif
// ✅ ToVector3() 사용 (유효성 검증 포함)
transform.eulerAngles = param.rotation.ToVector3();
return new { success = true };
}
private object HandleGetScale(JsonRpcRequest request)
{
var param = ValidateParam<NameParam>(request, "name");
var transform = GetTransform(param.name);
return new Vector3Data
{
x = transform.localScale.x,
y = transform.localScale.y,
z = transform.localScale.z
};
}
private object HandleSetScale(JsonRpcRequest request)
{
var param = ValidateParam<SetScaleParam>(request, "name and scale");
var transform = GetTransform(param.name);
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(transform, "Set Scale");
#endif
// ✅ ToVector3() 사용 (유효성 검증 포함)
transform.localScale = param.scale.ToVector3();
return new { success = true };
}
private Transform GetTransform(string name)
{
var obj = FindGameObject(name);
if (obj == null)
{
throw new Exception($"GameObject not found: {name}");
}
return obj.transform;
}
// Parameter classes (✅ private으로 변경)
[Serializable]
private class NameParam
{
public string name;
}
[Serializable]
private class SetPositionParam
{
public string name;
public Vector3Data position;
}
[Serializable]
private class SetRotationParam
{
public string name;
public Vector3Data rotation;
}
[Serializable]
private class SetScaleParam
{
public string name;
public Vector3Data scale;
}
// Response classes
[Serializable]
public class Vector3Data
{
public float x;
public float y;
public float z;
/// <summary>
/// Vector3로 변환 (✅ 유효성 검증 추가)
/// </summary>
public Vector3 ToVector3()
{
// NaN 및 Infinity 체크
if (float.IsNaN(x) || float.IsInfinity(x))
{
throw new ArgumentException($"Invalid x value: {x}");
}
if (float.IsNaN(y) || float.IsInfinity(y))
{
throw new ArgumentException($"Invalid y value: {y}");
}
if (float.IsNaN(z) || float.IsInfinity(z))
{
throw new ArgumentException($"Invalid z value: {z}");
}
return new Vector3(x, y, z);
}
/// <summary>
/// Vector3에서 생성
/// </summary>
public static Vector3Data FromVector3(Vector3 v)
{
return new Vector3Data { x = v.x, y = v.y, z = v.z };
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e4fd55a1342224c4799f08358b3f35c3