using System; using System.Collections.Generic; using System.Linq; using UnityEditorToolkit.Protocol; namespace UnityEditorToolkit.Handlers { /// /// Base handler for JSON-RPC commands /// public abstract class BaseHandler { /// /// GameObject 캐시 (WeakReference 사용하여 메모리 누수 방지) /// private static Dictionary gameObjectCache = new Dictionary(); private static readonly object cacheLock = new object(); /// /// Handler category (e.g., "GameObject", "Transform") /// public abstract string Category { get; } /// /// Handle JSON-RPC request /// /// JSON-RPC request /// Response object or null for error 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; } } /// /// Handle specific method (must be implemented by subclass) /// protected abstract object HandleMethod(string method, JsonRpcRequest request); /// /// Validate required parameter /// protected T ValidateParam(JsonRpcRequest request, string paramName) where T : class { var paramsObj = request.GetParams(); if (paramsObj == null) { throw new Exception($"Missing or invalid parameter: {paramName}"); } return paramsObj; } /// /// Find GameObject by name or path (캐싱 적용) /// 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(); 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; } /// /// GameObject를 캐시에 추가 /// 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(); 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); } } } } /// /// 캐시 비우기 (테스트용 또는 메모리 정리) /// public static void ClearCache() { lock (cacheLock) { gameObjectCache.Clear(); } } /// /// Get full path of GameObject in hierarchy /// 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; } } }