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,511 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Database;
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Analytics and caching
/// </summary>
public class AnalyticsHandler : BaseHandler
{
public override string Category => "Analytics";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "GetProjectStats":
return HandleGetProjectStats(request);
case "GetSceneStats":
return HandleGetSceneStats(request);
case "SetCache":
return HandleSetCache(request);
case "GetCache":
return HandleGetCache(request);
case "ClearCache":
return HandleClearCache(request);
case "ListCache":
return HandleListCache(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
/// <summary>
/// Get project-wide statistics
/// </summary>
private object HandleGetProjectStats(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
// Check cache first
var cacheKey = "project_stats";
var cacheResult = GetCacheData(connection, cacheKey);
if (cacheResult != null)
{
return JsonUtility.FromJson<ProjectStatsResult>(cacheResult);
}
// Calculate stats
int totalScenes = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM scenes");
int totalObjects = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM gameobjects WHERE is_deleted = 0");
int totalComponents = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM components");
int totalTransforms = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM transforms");
int totalSnapshots = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM snapshots");
int commandHistoryCount = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM command_history");
// Get most used components
var componentsSql = @"
SELECT component_type, COUNT(*) as count
FROM components
GROUP BY component_type
ORDER BY count DESC
LIMIT 10
";
var componentStats = connection.Query<ComponentStatRecord>(componentsSql);
var topComponents = componentStats.Select(c => new ComponentStat
{
componentType = c.component_type,
count = c.count
}).ToList();
var result = new ProjectStatsResult
{
success = true,
totalScenes = totalScenes,
totalObjects = totalObjects,
totalComponents = totalComponents,
totalTransforms = totalTransforms,
totalSnapshots = totalSnapshots,
commandHistoryCount = commandHistoryCount,
topComponents = topComponents
};
// Cache result
var jsonData = JsonUtility.ToJson(result);
SetCacheData(connection, cacheKey, jsonData, 3600); // Cache for 1 hour
return result;
}
/// <summary>
/// Get current scene statistics
/// </summary>
private object HandleGetSceneStats(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var scene = SceneManager.GetActiveScene();
var cacheKey = $"scene_stats_{scene.path}";
// Check cache
var cacheResult = GetCacheData(connection, cacheKey);
if (cacheResult != null)
{
return JsonUtility.FromJson<SceneStatsResult>(cacheResult);
}
// Get scene ID
var sceneIdSql = "SELECT scene_id FROM scenes WHERE scene_path = ?";
var sceneIds = connection.Query<SceneIdRecord>(sceneIdSql, scene.path);
if (sceneIds.Count() == 0)
{
return new SceneStatsResult
{
success = true,
sceneName = scene.name,
scenePath = scene.path,
objectCount = 0,
componentCount = 0,
snapshotCount = 0,
message = "Scene not synced to database"
};
}
int sceneId = sceneIds.First().scene_id;
// Calculate stats
int objectCount = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM gameobjects WHERE scene_id = ? AND is_deleted = 0", sceneId);
int componentCount = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM components WHERE object_id IN (SELECT object_id FROM gameobjects WHERE scene_id = ?)", sceneId);
int snapshotCount = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM snapshots WHERE scene_id = ?", sceneId);
int transformHistoryCount = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM transforms WHERE object_id IN (SELECT object_id FROM gameobjects WHERE scene_id = ?)", sceneId);
var result = new SceneStatsResult
{
success = true,
sceneName = scene.name,
scenePath = scene.path,
sceneId = sceneId,
objectCount = objectCount,
componentCount = componentCount,
snapshotCount = snapshotCount,
transformHistoryCount = transformHistoryCount,
message = "Scene statistics retrieved successfully"
};
// Cache result
var jsonData = JsonUtility.ToJson(result);
SetCacheData(connection, cacheKey, jsonData, 300); // Cache for 5 minutes
return result;
}
/// <summary>
/// Set cache data
/// </summary>
private object HandleSetCache(JsonRpcRequest request)
{
var param = ValidateParam<SetCacheParams>(request, "key");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
int ttl = param.ttl > 0 ? param.ttl : 3600; // Default 1 hour
SetCacheData(connection, param.key, param.data, ttl);
return new CacheResult
{
success = true,
key = param.key,
message = $"Cache set successfully (TTL: {ttl}s)"
};
}
/// <summary>
/// Get cache data
/// </summary>
private object HandleGetCache(JsonRpcRequest request)
{
var param = ValidateParam<GetCacheParams>(request, "key");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var data = GetCacheData(connection, param.key);
if (data == null)
{
return new GetCacheResult
{
success = false,
key = param.key,
data = null,
message = "Cache not found or expired"
};
}
return new GetCacheResult
{
success = true,
key = param.key,
data = data,
message = "Cache retrieved successfully"
};
}
/// <summary>
/// Clear cache
/// </summary>
private object HandleClearCache(JsonRpcRequest request)
{
var param = request.GetParams<ClearCacheParams>();
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
int deletedCount;
if (param != null && !string.IsNullOrEmpty(param.key))
{
// Clear specific key
deletedCount = connection.Execute("DELETE FROM analytics_cache WHERE cache_key = ?", param.key);
}
else
{
// Clear all cache
deletedCount = connection.Execute("DELETE FROM analytics_cache");
}
return new ClearCacheResult
{
success = true,
deletedCount = deletedCount,
message = $"Cleared {deletedCount} cache entries"
};
}
/// <summary>
/// List all cache entries
/// </summary>
private object HandleListCache(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var sql = @"
SELECT cache_id, cache_key, expires_at, created_at
FROM analytics_cache
ORDER BY created_at DESC
";
var records = connection.Query<CacheListRecord>(sql);
var entries = records.Select(r => new CacheEntry
{
cacheId = r.cache_id,
cacheKey = r.cache_key,
expiresAt = r.expires_at,
createdAt = r.created_at,
isExpired = IsExpired(r.expires_at)
}).ToList();
return new ListCacheResult
{
success = true,
count = entries.Count,
entries = entries
};
}
#region Helper Methods
private void SetCacheData(SQLite.SQLiteConnection connection, string key, string data, int ttlSeconds)
{
var expiresAt = DateTime.Now.AddSeconds(ttlSeconds).ToString("O");
// Delete existing
connection.Execute("DELETE FROM analytics_cache WHERE cache_key = ?", key);
// Insert new
var sql = @"
INSERT INTO analytics_cache (cache_key, cache_data, expires_at, created_at)
VALUES (?, ?, ?, datetime('now', 'localtime'))
";
connection.Execute(sql, key, data, expiresAt);
}
private string GetCacheData(SQLite.SQLiteConnection connection, string key)
{
var sql = "SELECT cache_data, expires_at FROM analytics_cache WHERE cache_key = ?";
var records = connection.Query<CacheDataRecord>(sql, key);
if (records.Count() == 0)
{
return null;
}
var record = records.First();
// Check expiration
if (IsExpired(record.expires_at))
{
// Delete expired cache
connection.Execute("DELETE FROM analytics_cache WHERE cache_key = ?", key);
return null;
}
return record.cache_data;
}
private bool IsExpired(string expiresAt)
{
if (string.IsNullOrEmpty(expiresAt))
{
return false; // Never expires
}
DateTime expires;
if (DateTime.TryParse(expiresAt, out expires))
{
return DateTime.Now > expires;
}
return false;
}
#endregion
#region Data Classes
private class SceneIdRecord
{
public int scene_id { get; set; }
}
private class ComponentStatRecord
{
public string component_type { get; set; }
public int count { get; set; }
}
private class CacheDataRecord
{
public string cache_data { get; set; }
public string expires_at { get; set; }
}
private class CacheListRecord
{
public int cache_id { get; set; }
public string cache_key { get; set; }
public string expires_at { get; set; }
public string created_at { get; set; }
}
[Serializable]
public class SetCacheParams
{
public string key;
public string data;
public int ttl = 3600; // Default 1 hour
}
[Serializable]
public class GetCacheParams
{
public string key;
}
[Serializable]
public class ClearCacheParams
{
public string key;
}
[Serializable]
public class ComponentStat
{
public string componentType;
public int count;
}
[Serializable]
public class ProjectStatsResult
{
public bool success;
public int totalScenes;
public int totalObjects;
public int totalComponents;
public int totalTransforms;
public int totalSnapshots;
public int commandHistoryCount;
public List<ComponentStat> topComponents;
}
[Serializable]
public class SceneStatsResult
{
public bool success;
public string sceneName;
public string scenePath;
public int sceneId;
public int objectCount;
public int componentCount;
public int snapshotCount;
public int transformHistoryCount;
public string message;
}
[Serializable]
public class CacheResult
{
public bool success;
public string key;
public string message;
}
[Serializable]
public class GetCacheResult
{
public bool success;
public string key;
public string data;
public string message;
}
[Serializable]
public class ClearCacheResult
{
public bool success;
public int deletedCount;
public string message;
}
[Serializable]
public class CacheEntry
{
public int cacheId;
public string cacheKey;
public string expiresAt;
public string createdAt;
public bool isExpired;
}
[Serializable]
public class ListCacheResult
{
public bool success;
public int count;
public List<CacheEntry> entries;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,566 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditorToolkit.Protocol;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Animation commands
/// </summary>
public class AnimationHandler : BaseHandler
{
public override string Category => "Animation";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Play":
return HandlePlay(request);
case "Stop":
return HandleStop(request);
case "GetState":
return HandleGetState(request);
case "SetParameter":
return HandleSetParameter(request);
case "GetParameter":
return HandleGetParameter(request);
case "GetParameters":
return HandleGetParameters(request);
case "SetTrigger":
return HandleSetTrigger(request);
case "ResetTrigger":
return HandleResetTrigger(request);
case "CrossFade":
return HandleCrossFade(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
private object HandlePlay(JsonRpcRequest request)
{
var param = ValidateParam<AnimationPlayParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
// Try Animator first (newer system)
var animator = obj.GetComponent<Animator>();
if (animator != null)
{
if (!string.IsNullOrEmpty(param.stateName))
{
animator.Play(param.stateName, param.layer ?? 0, param.normalizedTime ?? 0f);
}
else
{
animator.enabled = true;
animator.speed = param.speed ?? 1f;
}
return new
{
success = true,
gameObject = param.gameObject,
type = "Animator",
stateName = param.stateName ?? "Default",
message = "Animation playing"
};
}
// Fall back to legacy Animation component
var animation = obj.GetComponent<Animation>();
if (animation != null)
{
if (!string.IsNullOrEmpty(param.clipName))
{
animation.Play(param.clipName);
}
else
{
animation.Play();
}
return new
{
success = true,
gameObject = param.gameObject,
type = "Animation",
clipName = param.clipName ?? "Default",
message = "Animation playing"
};
}
throw new Exception($"No Animator or Animation component found on: {param.gameObject}");
}
private object HandleStop(JsonRpcRequest request)
{
var param = ValidateParam<AnimationStopParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
// Try Animator first
var animator = obj.GetComponent<Animator>();
if (animator != null)
{
if (param.resetToDefault)
{
animator.Rebind();
animator.Update(0f);
}
animator.speed = 0f;
return new
{
success = true,
gameObject = param.gameObject,
type = "Animator",
message = "Animation stopped"
};
}
// Fall back to legacy Animation
var animation = obj.GetComponent<Animation>();
if (animation != null)
{
if (!string.IsNullOrEmpty(param.clipName))
{
animation.Stop(param.clipName);
}
else
{
animation.Stop();
}
return new
{
success = true,
gameObject = param.gameObject,
type = "Animation",
message = "Animation stopped"
};
}
throw new Exception($"No Animator or Animation component found on: {param.gameObject}");
}
private object HandleGetState(JsonRpcRequest request)
{
var param = ValidateParam<AnimationStateParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
// Try Animator first
var animator = obj.GetComponent<Animator>();
if (animator != null)
{
int layer = param.layer ?? 0;
var stateInfo = animator.GetCurrentAnimatorStateInfo(layer);
var clipInfo = animator.GetCurrentAnimatorClipInfo(layer);
string currentClipName = "None";
if (clipInfo.Length > 0 && clipInfo[0].clip != null)
{
currentClipName = clipInfo[0].clip.name;
}
return new
{
success = true,
gameObject = param.gameObject,
type = "Animator",
enabled = animator.enabled,
speed = animator.speed,
layer = layer,
currentState = new
{
fullPathHash = stateInfo.fullPathHash,
shortNameHash = stateInfo.shortNameHash,
normalizedTime = stateInfo.normalizedTime,
length = stateInfo.length,
speed = stateInfo.speed,
speedMultiplier = stateInfo.speedMultiplier,
isLooping = stateInfo.loop,
clipName = currentClipName
},
isInTransition = animator.IsInTransition(layer),
hasRootMotion = animator.hasRootMotion,
layerCount = animator.layerCount,
parameterCount = animator.parameterCount
};
}
// Fall back to legacy Animation
var animation = obj.GetComponent<Animation>();
if (animation != null)
{
var clips = new List<object>();
foreach (AnimationState state in animation)
{
clips.Add(new
{
name = state.name,
length = state.length,
normalizedTime = state.normalizedTime,
speed = state.speed,
weight = state.weight,
enabled = state.enabled,
wrapMode = state.wrapMode.ToString()
});
}
return new
{
success = true,
gameObject = param.gameObject,
type = "Animation",
isPlaying = animation.isPlaying,
clipCount = animation.GetClipCount(),
clips = clips
};
}
throw new Exception($"No Animator or Animation component found on: {param.gameObject}");
}
private object HandleSetParameter(JsonRpcRequest request)
{
var param = ValidateParam<AnimationParameterParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var animator = obj.GetComponent<Animator>();
if (animator == null)
{
throw new Exception($"No Animator component found on: {param.gameObject}");
}
// Find parameter type
AnimatorControllerParameterType? paramType = null;
foreach (var p in animator.parameters)
{
if (p.name == param.parameterName)
{
paramType = p.type;
break;
}
}
if (!paramType.HasValue)
{
throw new Exception($"Parameter not found: {param.parameterName}");
}
switch (paramType.Value)
{
case AnimatorControllerParameterType.Float:
animator.SetFloat(param.parameterName, Convert.ToSingle(param.value));
break;
case AnimatorControllerParameterType.Int:
animator.SetInteger(param.parameterName, Convert.ToInt32(param.value));
break;
case AnimatorControllerParameterType.Bool:
animator.SetBool(param.parameterName, Convert.ToBoolean(param.value));
break;
case AnimatorControllerParameterType.Trigger:
if (Convert.ToBoolean(param.value))
animator.SetTrigger(param.parameterName);
else
animator.ResetTrigger(param.parameterName);
break;
}
return new
{
success = true,
gameObject = param.gameObject,
parameterName = param.parameterName,
parameterType = paramType.Value.ToString(),
value = param.value
};
}
private object HandleGetParameter(JsonRpcRequest request)
{
var param = ValidateParam<AnimationGetParameterParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var animator = obj.GetComponent<Animator>();
if (animator == null)
{
throw new Exception($"No Animator component found on: {param.gameObject}");
}
// Find parameter
AnimatorControllerParameter foundParam = null;
foreach (var p in animator.parameters)
{
if (p.name == param.parameterName)
{
foundParam = p;
break;
}
}
if (foundParam == null)
{
throw new Exception($"Parameter not found: {param.parameterName}");
}
object value = null;
switch (foundParam.type)
{
case AnimatorControllerParameterType.Float:
value = animator.GetFloat(param.parameterName);
break;
case AnimatorControllerParameterType.Int:
value = animator.GetInteger(param.parameterName);
break;
case AnimatorControllerParameterType.Bool:
value = animator.GetBool(param.parameterName);
break;
case AnimatorControllerParameterType.Trigger:
value = "Trigger (no value)";
break;
}
return new
{
success = true,
gameObject = param.gameObject,
parameterName = param.parameterName,
parameterType = foundParam.type.ToString(),
value = value
};
}
private object HandleGetParameters(JsonRpcRequest request)
{
var param = ValidateParam<AnimationBaseParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var animator = obj.GetComponent<Animator>();
if (animator == null)
{
throw new Exception($"No Animator component found on: {param.gameObject}");
}
var parameters = new List<object>();
foreach (var p in animator.parameters)
{
object value = null;
switch (p.type)
{
case AnimatorControllerParameterType.Float:
value = animator.GetFloat(p.name);
break;
case AnimatorControllerParameterType.Int:
value = animator.GetInteger(p.name);
break;
case AnimatorControllerParameterType.Bool:
value = animator.GetBool(p.name);
break;
case AnimatorControllerParameterType.Trigger:
value = null;
break;
}
parameters.Add(new
{
name = p.name,
type = p.type.ToString(),
value = value,
defaultFloat = p.defaultFloat,
defaultInt = p.defaultInt,
defaultBool = p.defaultBool
});
}
return new
{
success = true,
gameObject = param.gameObject,
count = parameters.Count,
parameters = parameters
};
}
private object HandleSetTrigger(JsonRpcRequest request)
{
var param = ValidateParam<AnimationTriggerParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var animator = obj.GetComponent<Animator>();
if (animator == null)
{
throw new Exception($"No Animator component found on: {param.gameObject}");
}
animator.SetTrigger(param.triggerName);
return new
{
success = true,
gameObject = param.gameObject,
triggerName = param.triggerName,
message = "Trigger set"
};
}
private object HandleResetTrigger(JsonRpcRequest request)
{
var param = ValidateParam<AnimationTriggerParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var animator = obj.GetComponent<Animator>();
if (animator == null)
{
throw new Exception($"No Animator component found on: {param.gameObject}");
}
animator.ResetTrigger(param.triggerName);
return new
{
success = true,
gameObject = param.gameObject,
triggerName = param.triggerName,
message = "Trigger reset"
};
}
private object HandleCrossFade(JsonRpcRequest request)
{
var param = ValidateParam<AnimationCrossFadeParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var animator = obj.GetComponent<Animator>();
if (animator == null)
{
throw new Exception($"No Animator component found on: {param.gameObject}");
}
animator.CrossFade(
param.stateName,
param.transitionDuration ?? 0.25f,
param.layer ?? 0,
param.normalizedTimeOffset ?? 0f
);
return new
{
success = true,
gameObject = param.gameObject,
stateName = param.stateName,
transitionDuration = param.transitionDuration ?? 0.25f,
message = "CrossFade started"
};
}
// Parameter classes
[Serializable]
public class AnimationBaseParams
{
public string gameObject;
}
[Serializable]
public class AnimationPlayParams : AnimationBaseParams
{
public string stateName; // For Animator
public string clipName; // For legacy Animation
public int? layer;
public float? normalizedTime;
public float? speed;
}
[Serializable]
public class AnimationStopParams : AnimationBaseParams
{
public string clipName;
public bool resetToDefault;
}
[Serializable]
public class AnimationStateParams : AnimationBaseParams
{
public int? layer;
}
[Serializable]
public class AnimationParameterParams : AnimationBaseParams
{
public string parameterName;
public object value;
}
[Serializable]
public class AnimationGetParameterParams : AnimationBaseParams
{
public string parameterName;
}
[Serializable]
public class AnimationTriggerParams : AnimationBaseParams
{
public string triggerName;
}
[Serializable]
public class AnimationCrossFadeParams : AnimationBaseParams
{
public string stateName;
public float? transitionDuration;
public int? layer;
public float? normalizedTimeOffset;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 95678540617b4de439b05e447c7b1a8a

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 13f4614dd3790eb418c810975f0eb5cc

View File

@@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Utils;
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)
{
ToolkitLogger.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: 4bd9849f6e036df4787b825d4a713960

View File

@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Utils;
using Newtonsoft.Json.Linq;
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Chain commands
/// Executes multiple commands sequentially
/// </summary>
public class ChainHandler : BaseHandler
{
public override string Category => "Chain";
private Dictionary<string, BaseHandler> handlers;
public void SetHandlers(Dictionary<string, BaseHandler> handlers)
{
this.handlers = handlers;
}
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Execute":
return HandleExecute(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
private object HandleExecute(JsonRpcRequest request)
{
var param = ValidateParam<ExecuteParams>(request, "commands");
if (param.commands == null || param.commands.Length == 0)
{
throw new Exception("Commands array is required and cannot be empty");
}
ToolkitLogger.Log("ChainHandler", $"Executing {param.commands.Length} command(s) sequentially...");
var results = new List<object>();
double totalElapsed = 0;
for (int i = 0; i < param.commands.Length; i++)
{
var cmd = param.commands[i];
double startTime = UnityEditor.EditorApplication.timeSinceStartup;
try
{
ToolkitLogger.LogDebug("ChainHandler", $"[{i + 1}/{param.commands.Length}] Executing: {cmd.method}");
// Parse method to get category
string category = GetCategory(cmd.method);
if (!handlers.ContainsKey(category))
{
throw new Exception($"Unknown command category: {category}");
}
// Create a new request for this command
var chainedRequest = new JsonRpcRequest
{
Id = $"{request.Id}:chain:{i}",
Method = cmd.method,
Params = cmd.parameters != null ? JToken.FromObject(cmd.parameters) : null
};
// Execute the command
var handler = handlers[category];
var result = handler.Handle(chainedRequest);
double elapsed = UnityEditor.EditorApplication.timeSinceStartup - startTime;
totalElapsed += elapsed;
// If result is null, it's a delayed response (not supported in chain)
if (result == null)
{
throw new Exception($"Command '{cmd.method}' returned delayed response, which is not supported in chain execution");
}
results.Add(new
{
index = i,
method = cmd.method,
success = true,
result = result,
elapsed = elapsed
});
ToolkitLogger.LogDebug("ChainHandler", $"[{i + 1}/{param.commands.Length}] Success: {cmd.method} ({elapsed:F3}s)");
}
catch (Exception ex)
{
double elapsed = UnityEditor.EditorApplication.timeSinceStartup - startTime;
totalElapsed += elapsed;
ToolkitLogger.LogError("ChainHandler", $"[{i + 1}/{param.commands.Length}] Failed: {cmd.method} - {ex.Message}");
// Add error result
results.Add(new
{
index = i,
method = cmd.method,
success = false,
error = ex.Message,
elapsed = elapsed
});
// If stopOnError is true, stop execution
if (param.stopOnError)
{
ToolkitLogger.LogWarning("ChainHandler", "Stopping chain execution due to error (stopOnError=true)");
break;
}
}
}
return new
{
success = true,
totalCommands = param.commands.Length,
executedCommands = results.Count,
totalElapsed = totalElapsed,
results = results
};
}
private string GetCategory(string method)
{
int dotIndex = method.IndexOf('.');
if (dotIndex < 0)
{
throw new Exception($"Invalid method format: {method}");
}
return method.Substring(0, dotIndex);
}
// Parameter classes
[Serializable]
public class ExecuteParams
{
public CommandEntry[] commands;
public bool stopOnError = true;
}
[Serializable]
public class CommandEntry
{
public string method;
public object parameters;
}
}
}

View File

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

View File

@@ -0,0 +1,973 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditorToolkit.Protocol;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Component commands (add, remove, enable, disable, get, set, inspect, move-up, move-down, copy)
/// </summary>
public class ComponentHandler : BaseHandler
{
public override string Category => "Component";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "List":
return HandleList(request);
case "Add":
return HandleAdd(request);
case "Remove":
return HandleRemove(request);
case "SetEnabled":
return HandleSetEnabled(request);
case "Get":
return HandleGet(request);
case "Set":
return HandleSet(request);
case "Inspect":
return HandleInspect(request);
case "MoveUp":
return HandleMoveUp(request);
case "MoveDown":
return HandleMoveDown(request);
case "Copy":
return HandleCopy(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
/// <summary>
/// List all components on a GameObject
/// </summary>
private object HandleList(JsonRpcRequest request)
{
var param = ValidateParam<ListParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
Component[] components = obj.GetComponents<Component>();
var list = new List<ComponentInfo>();
foreach (var comp in components)
{
if (comp == null) continue;
// Check enabled state (only for Behaviour types)
bool isEnabled = true;
if (comp is Behaviour behaviour)
{
isEnabled = behaviour.enabled;
}
// Skip disabled components if not requested
if (!param.includeDisabled && !isEnabled)
continue;
list.Add(new ComponentInfo
{
type = comp.GetType().Name,
fullTypeName = comp.GetType().FullName,
enabled = isEnabled,
isMonoBehaviour = comp is MonoBehaviour
});
}
return new ComponentListResult { count = list.Count, components = list };
}
/// <summary>
/// Add a component to a GameObject
/// </summary>
private object HandleAdd(JsonRpcRequest request)
{
var param = ValidateParam<AddParams>(request, "name and componentType");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
// Find component type
Type componentType = FindComponentType(param.componentType);
if (componentType == null)
{
throw new Exception($"Component type not found: {param.componentType}");
}
// Check if component already exists
if (obj.GetComponent(componentType) != null)
{
throw new Exception($"Component already exists: {param.componentType}");
}
// Add component
Component comp = obj.AddComponent(componentType);
// Register undo
#if UNITY_EDITOR
Undo.RegisterCreatedObjectUndo(comp, "Add Component");
#endif
return new ComponentInfo
{
type = comp.GetType().Name,
fullTypeName = comp.GetType().FullName,
enabled = comp is Behaviour ? ((Behaviour)comp).enabled : true,
isMonoBehaviour = comp is MonoBehaviour
};
}
/// <summary>
/// Remove a component from a GameObject
/// </summary>
private object HandleRemove(JsonRpcRequest request)
{
var param = ValidateParam<RemoveParams>(request, "name and componentType");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
// Find component type
Type componentType = FindComponentType(param.componentType);
if (componentType == null)
{
throw new Exception($"Component type not found: {param.componentType}");
}
// Protect Transform
if (componentType == typeof(Transform))
{
throw new Exception("Cannot remove Transform component (required on all GameObjects)");
}
// Find component
Component comp = obj.GetComponent(componentType);
if (comp == null)
{
throw new Exception($"Component not found: {param.componentType}");
}
// Remove component
#if UNITY_EDITOR
Undo.DestroyObjectImmediate(comp);
#else
Object.DestroyImmediate(comp);
#endif
return new { success = true };
}
/// <summary>
/// Enable or disable a component
/// </summary>
private object HandleSetEnabled(JsonRpcRequest request)
{
var param = ValidateParam<SetEnabledParams>(request, "name, componentType and enabled");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
// Find component type
Type componentType = FindComponentType(param.componentType);
if (componentType == null)
{
throw new Exception($"Component type not found: {param.componentType}");
}
// Find component
Component comp = obj.GetComponent(componentType);
if (comp == null)
{
throw new Exception($"Component not found: {param.componentType}");
}
// Only Behaviour components support enabled property
if (!(comp is Behaviour behaviour))
{
throw new Exception($"Component {param.componentType} does not support enabled property");
}
#if UNITY_EDITOR
Undo.RegisterCompleteObjectUndo(comp, param.enabled ? "Enable Component" : "Disable Component");
#endif
behaviour.enabled = param.enabled;
return new { success = true, enabled = behaviour.enabled };
}
/// <summary>
/// Get component properties
/// </summary>
private object HandleGet(JsonRpcRequest request)
{
var param = ValidateParam<GetParams>(request, "name and componentType");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
// Find component type
Type componentType = FindComponentType(param.componentType);
if (componentType == null)
{
throw new Exception($"Component type not found: {param.componentType}");
}
// Find component
Component comp = obj.GetComponent(componentType);
if (comp == null)
{
throw new Exception($"Component not found: {param.componentType}");
}
#if UNITY_EDITOR
SerializedObject so = new SerializedObject(comp);
// Get specific property
if (!string.IsNullOrEmpty(param.property))
{
SerializedProperty prop = so.FindProperty(param.property);
if (prop == null)
{
throw new Exception($"Property not found: {param.property}");
}
return new PropertyInfo
{
name = param.property,
type = prop.propertyType.ToString(),
value = GetPropertyValue(prop)
};
}
// Get all properties
var properties = new List<PropertyInfo>();
SerializedProperty iterator = so.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
// Skip internal properties
if (iterator.name.StartsWith("m_"))
continue;
properties.Add(new PropertyInfo
{
name = iterator.name,
type = iterator.propertyType.ToString(),
value = GetPropertyValue(iterator)
});
}
return new GetComponentResult
{
componentType = param.componentType,
properties = properties
};
#else
throw new Exception("Component.Get is only available in Editor mode");
#endif
}
/// <summary>
/// Set a component property
/// </summary>
private object HandleSet(JsonRpcRequest request)
{
var param = ValidateParam<SetParams>(request, "name, componentType, property and value");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
// Find component type
Type componentType = FindComponentType(param.componentType);
if (componentType == null)
{
throw new Exception($"Component type not found: {param.componentType}");
}
// Find component
Component comp = obj.GetComponent(componentType);
if (comp == null)
{
throw new Exception($"Component not found: {param.componentType}");
}
#if UNITY_EDITOR
SerializedObject so = new SerializedObject(comp);
SerializedProperty prop = so.FindProperty(param.property);
if (prop == null)
{
throw new Exception($"Property not found: {param.property}");
}
// Register undo
Undo.RegisterCompleteObjectUndo(comp, "Set Component Property");
object oldValue = GetPropertyValue(prop);
// Set property value
try
{
SetPropertyValue(prop, param.value);
so.ApplyModifiedProperties();
}
catch (Exception ex)
{
throw new Exception($"Failed to set property: {ex.Message}");
}
return new SetPropertyResult
{
success = true,
property = param.property,
oldValue = oldValue,
newValue = param.value
};
#else
throw new Exception("Component.Set is only available in Editor mode");
#endif
}
/// <summary>
/// Inspect a component (get all properties and state)
/// </summary>
private object HandleInspect(JsonRpcRequest request)
{
var param = ValidateParam<InspectParams>(request, "name and componentType");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
// Find component type
Type componentType = FindComponentType(param.componentType);
if (componentType == null)
{
throw new Exception($"Component type not found: {param.componentType}");
}
// Find component
Component comp = obj.GetComponent(componentType);
if (comp == null)
{
throw new Exception($"Component not found: {param.componentType}");
}
#if UNITY_EDITOR
SerializedObject so = new SerializedObject(comp);
var properties = new List<PropertyInfo>();
SerializedProperty iterator = so.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
// Include all properties for inspection
properties.Add(new PropertyInfo
{
name = iterator.name,
type = iterator.propertyType.ToString(),
value = GetPropertyValue(iterator)
});
}
bool isEnabled = comp is Behaviour ? ((Behaviour)comp).enabled : true;
return new InspectComponentResult
{
componentType = param.componentType,
fullTypeName = comp.GetType().FullName,
enabled = isEnabled,
isMonoBehaviour = comp is MonoBehaviour,
properties = properties,
propertyCount = properties.Count
};
#else
throw new Exception("Component.Inspect is only available in Editor mode");
#endif
}
/// <summary>
/// Move a component up in the component list
/// </summary>
private object HandleMoveUp(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<MoveParams>(request, "name and componentType");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
// Find component type
Type componentType = FindComponentType(param.componentType);
if (componentType == null)
{
throw new Exception($"Component type not found: {param.componentType}");
}
// Find component
Component comp = obj.GetComponent(componentType);
if (comp == null)
{
throw new Exception($"Component not found: {param.componentType}");
}
// Use ComponentUtility to move component
bool success = UnityEditorInternal.ComponentUtility.MoveComponentUp(comp);
if (!success)
{
throw new Exception("Cannot move component up (already at top or is Transform)");
}
return new { success = true };
#else
throw new Exception("Component.MoveUp is only available in Editor mode");
#endif
}
/// <summary>
/// Move a component down in the component list
/// </summary>
private object HandleMoveDown(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<MoveParams>(request, "name and componentType");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
// Find component type
Type componentType = FindComponentType(param.componentType);
if (componentType == null)
{
throw new Exception($"Component type not found: {param.componentType}");
}
// Find component
Component comp = obj.GetComponent(componentType);
if (comp == null)
{
throw new Exception($"Component not found: {param.componentType}");
}
// Use ComponentUtility to move component
bool success = UnityEditorInternal.ComponentUtility.MoveComponentDown(comp);
if (!success)
{
throw new Exception("Cannot move component down (already at bottom)");
}
return new { success = true };
#else
throw new Exception("Component.MoveDown is only available in Editor mode");
#endif
}
/// <summary>
/// Copy a component from one GameObject to another
/// </summary>
private object HandleCopy(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<CopyParams>(request, "source, componentType and target");
var sourceObj = FindGameObject(param.source);
if (sourceObj == null)
{
throw new Exception($"Source GameObject not found: {param.source}");
}
var targetObj = FindGameObject(param.target);
if (targetObj == null)
{
throw new Exception($"Target GameObject not found: {param.target}");
}
// Find component type
Type componentType = FindComponentType(param.componentType);
if (componentType == null)
{
throw new Exception($"Component type not found: {param.componentType}");
}
// Find component on source
Component sourceComp = sourceObj.GetComponent(componentType);
if (sourceComp == null)
{
throw new Exception($"Component not found on source GameObject: {param.componentType}");
}
// Use ComponentUtility to copy component
bool success = UnityEditorInternal.ComponentUtility.CopyComponent(sourceComp);
if (!success)
{
throw new Exception("Failed to copy component");
}
success = UnityEditorInternal.ComponentUtility.PasteComponentAsNew(targetObj);
if (!success)
{
throw new Exception("Failed to paste component");
}
return new { success = true };
#else
throw new Exception("Component.Copy is only available in Editor mode");
#endif
}
/// <summary>
/// Find a component type by name (with multiple fallback strategies)
/// </summary>
private Type FindComponentType(string typeName)
{
if (string.IsNullOrWhiteSpace(typeName))
{
return null;
}
// 1. Try exact type with namespace
var type = Type.GetType(typeName);
if (type != null && typeof(Component).IsAssignableFrom(type))
{
return type;
}
// 2. Try UnityEngine namespace
type = Type.GetType($"UnityEngine.{typeName}, UnityEngine");
if (type != null && typeof(Component).IsAssignableFrom(type))
{
return type;
}
// 3. Scan all assemblies
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
type = assembly.GetType(typeName);
if (type != null && typeof(Component).IsAssignableFrom(type))
{
return type;
}
}
return null;
}
/// <summary>
/// Get the value of a serialized property
/// </summary>
private object GetPropertyValue(SerializedProperty prop)
{
switch (prop.propertyType)
{
case SerializedPropertyType.Integer:
return prop.intValue;
case SerializedPropertyType.Float:
return prop.floatValue;
case SerializedPropertyType.Boolean:
return prop.boolValue;
case SerializedPropertyType.String:
return prop.stringValue;
case SerializedPropertyType.Vector2:
return new { x = prop.vector2Value.x, y = prop.vector2Value.y };
case SerializedPropertyType.Vector3:
return new { x = prop.vector3Value.x, y = prop.vector3Value.y, z = prop.vector3Value.z };
case SerializedPropertyType.Vector4:
return new { x = prop.vector4Value.x, y = prop.vector4Value.y, z = prop.vector4Value.z, w = prop.vector4Value.w };
case SerializedPropertyType.Rect:
return new { x = prop.rectValue.x, y = prop.rectValue.y, width = prop.rectValue.width, height = prop.rectValue.height };
case SerializedPropertyType.ArraySize:
return prop.arraySize;
case SerializedPropertyType.Color:
return new { r = prop.colorValue.r, g = prop.colorValue.g, b = prop.colorValue.b, a = prop.colorValue.a };
case SerializedPropertyType.ObjectReference:
return prop.objectReferenceValue != null ? prop.objectReferenceValue.name : "null";
case SerializedPropertyType.Enum:
return prop.enumValueIndex >= 0 && prop.enumValueIndex < prop.enumNames.Length
? prop.enumNames[prop.enumValueIndex]
: prop.enumValueIndex.ToString();
case SerializedPropertyType.Vector2Int:
return new { x = prop.vector2IntValue.x, y = prop.vector2IntValue.y };
case SerializedPropertyType.Vector3Int:
return new { x = prop.vector3IntValue.x, y = prop.vector3IntValue.y, z = prop.vector3IntValue.z };
case SerializedPropertyType.RectInt:
return new { x = prop.rectIntValue.x, y = prop.rectIntValue.y, width = prop.rectIntValue.width, height = prop.rectIntValue.height };
case SerializedPropertyType.Bounds:
return new {
center = new { x = prop.boundsValue.center.x, y = prop.boundsValue.center.y, z = prop.boundsValue.center.z },
size = new { x = prop.boundsValue.size.x, y = prop.boundsValue.size.y, z = prop.boundsValue.size.z }
};
case SerializedPropertyType.BoundsInt:
return new {
position = new { x = prop.boundsIntValue.position.x, y = prop.boundsIntValue.position.y, z = prop.boundsIntValue.position.z },
size = new { x = prop.boundsIntValue.size.x, y = prop.boundsIntValue.size.y, z = prop.boundsIntValue.size.z }
};
default:
return prop.propertyType.ToString();
}
}
/// <summary>
/// Set the value of a serialized property
/// </summary>
private void SetPropertyValue(SerializedProperty prop, string value)
{
try
{
switch (prop.propertyType)
{
case SerializedPropertyType.Integer:
prop.intValue = int.Parse(value);
break;
case SerializedPropertyType.Float:
prop.floatValue = float.Parse(value);
break;
case SerializedPropertyType.Boolean:
prop.boolValue = bool.Parse(value);
break;
case SerializedPropertyType.String:
prop.stringValue = value;
break;
case SerializedPropertyType.Vector2:
ParseVector2(prop, value);
break;
case SerializedPropertyType.Vector3:
ParseVector3(prop, value);
break;
case SerializedPropertyType.Vector4:
ParseVector4(prop, value);
break;
case SerializedPropertyType.Rect:
ParseRect(prop, value);
break;
case SerializedPropertyType.Color:
ParseColor(prop, value);
break;
case SerializedPropertyType.Enum:
prop.enumValueIndex = System.Array.IndexOf(prop.enumNames, value);
if (prop.enumValueIndex < 0)
{
throw new Exception($"Enum value not found: {value}");
}
break;
case SerializedPropertyType.Vector2Int:
ParseVector2Int(prop, value);
break;
case SerializedPropertyType.Vector3Int:
ParseVector3Int(prop, value);
break;
case SerializedPropertyType.RectInt:
ParseRectInt(prop, value);
break;
case SerializedPropertyType.ObjectReference:
ParseObjectReference(prop, value);
break;
default:
throw new Exception($"Unsupported property type: {prop.propertyType}");
}
}
catch (Exception ex)
{
throw new Exception($"Failed to parse value for {prop.propertyType}: {ex.Message}");
}
}
private void ParseVector2(SerializedProperty prop, string value)
{
var parts = value.Split(',');
if (parts.Length != 2) throw new Exception("Vector2 requires 2 values");
prop.vector2Value = new Vector2(float.Parse(parts[0]), float.Parse(parts[1]));
}
private void ParseVector3(SerializedProperty prop, string value)
{
var parts = value.Split(',');
if (parts.Length != 3) throw new Exception("Vector3 requires 3 values");
prop.vector3Value = new Vector3(float.Parse(parts[0]), float.Parse(parts[1]), float.Parse(parts[2]));
}
private void ParseVector4(SerializedProperty prop, string value)
{
var parts = value.Split(',');
if (parts.Length != 4) throw new Exception("Vector4 requires 4 values");
prop.vector4Value = new Vector4(float.Parse(parts[0]), float.Parse(parts[1]), float.Parse(parts[2]), float.Parse(parts[3]));
}
private void ParseRect(SerializedProperty prop, string value)
{
var parts = value.Split(',');
if (parts.Length != 4) throw new Exception("Rect requires 4 values");
prop.rectValue = new Rect(float.Parse(parts[0]), float.Parse(parts[1]), float.Parse(parts[2]), float.Parse(parts[3]));
}
private void ParseColor(SerializedProperty prop, string value)
{
var parts = value.Split(',');
if (parts.Length < 3 || parts.Length > 4) throw new Exception("Color requires 3-4 values");
float a = parts.Length == 4 ? float.Parse(parts[3]) : 1f;
prop.colorValue = new Color(float.Parse(parts[0]), float.Parse(parts[1]), float.Parse(parts[2]), a);
}
private void ParseVector2Int(SerializedProperty prop, string value)
{
var parts = value.Split(',');
if (parts.Length != 2) throw new Exception("Vector2Int requires 2 values");
prop.vector2IntValue = new Vector2Int(int.Parse(parts[0]), int.Parse(parts[1]));
}
private void ParseVector3Int(SerializedProperty prop, string value)
{
var parts = value.Split(',');
if (parts.Length != 3) throw new Exception("Vector3Int requires 3 values");
prop.vector3IntValue = new Vector3Int(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[2]));
}
private void ParseRectInt(SerializedProperty prop, string value)
{
var parts = value.Split(',');
if (parts.Length != 4) throw new Exception("RectInt requires 4 values");
prop.rectIntValue = new RectInt(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[2]), int.Parse(parts[3]));
}
private void ParseObjectReference(SerializedProperty prop, string value)
{
// Handle null/empty values
if (string.IsNullOrEmpty(value) || value.ToLower() == "null")
{
prop.objectReferenceValue = null;
return;
}
// Check for "GameObject:Component" format (e.g., "GameHUD:UIDocument")
if (value.Contains(":"))
{
var parts = value.Split(':');
if (parts.Length == 2)
{
var goName = parts[0].Trim();
var compTypeName = parts[1].Trim();
var targetGo = FindGameObject(goName);
if (targetGo != null)
{
var compType = FindComponentType(compTypeName);
if (compType != null)
{
var comp = targetGo.GetComponent(compType);
if (comp != null)
{
prop.objectReferenceValue = comp;
return;
}
throw new Exception($"Component '{compTypeName}' not found on GameObject '{goName}'");
}
throw new Exception($"Component type not found: '{compTypeName}'");
}
throw new Exception($"GameObject not found: '{goName}'");
}
}
// Try to find GameObject by name
var foundGameObject = GameObject.Find(value);
if (foundGameObject != null)
{
prop.objectReferenceValue = foundGameObject;
return;
}
// Try to find in all scene objects
var allObjects = Resources.FindObjectsOfTypeAll<GameObject>();
foreach (var obj in allObjects)
{
if (obj.name == value && obj.scene.IsValid())
{
prop.objectReferenceValue = obj;
return;
}
}
// Try to load as asset
#if UNITY_EDITOR
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(value);
if (asset != null)
{
prop.objectReferenceValue = asset;
return;
}
#endif
// If nothing found, throw error with suggestions
throw new Exception($"ObjectReference not found: '{value}'. Try GameObject name, 'GameObject:Component' format, or asset path.");
}
#region Parameter Classes
[Serializable]
private class ListParams
{
public string name;
public bool includeDisabled;
}
[Serializable]
private class AddParams
{
public string name;
public string componentType;
}
[Serializable]
private class RemoveParams
{
public string name;
public string componentType;
}
[Serializable]
private class SetEnabledParams
{
public string name;
public string componentType;
public bool enabled;
}
[Serializable]
private class GetParams
{
public string name;
public string componentType;
public string property;
}
[Serializable]
private class SetParams
{
public string name;
public string componentType;
public string property;
public string value;
}
[Serializable]
private class InspectParams
{
public string name;
public string componentType;
}
[Serializable]
private class MoveParams
{
public string name;
public string componentType;
}
[Serializable]
private class CopyParams
{
public string source;
public string componentType;
public string target;
}
#endregion
#region Response Classes
[Serializable]
public class ComponentInfo
{
public string type;
public string fullTypeName;
public bool enabled;
public bool isMonoBehaviour;
}
[Serializable]
public class ComponentListResult
{
public int count;
public List<ComponentInfo> components;
}
[Serializable]
public class PropertyInfo
{
public string name;
public string type;
public object value;
}
[Serializable]
public class GetComponentResult
{
public string componentType;
public List<PropertyInfo> properties;
}
[Serializable]
public class SetPropertyResult
{
public bool success;
public string property;
public object oldValue;
public object newValue;
}
[Serializable]
public class InspectComponentResult
{
public string componentType;
public string fullTypeName;
public bool enabled;
public bool isMonoBehaviour;
public List<PropertyInfo> properties;
public int propertyCount;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9596e53f4c5aa9141a05dc71f672b3e7

View File

@@ -0,0 +1,321 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Utils;
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 };
var logs = new List<ConsoleLogEntry>();
#if UNITY_EDITOR
// Get logs from Unity Editor Console using Reflection
try
{
var assembly = Assembly.GetAssembly(typeof(UnityEditor.Editor));
if (assembly != null)
{
var logEntriesType = assembly.GetType("UnityEditor.LogEntries");
var logEntryType = assembly.GetType("UnityEditor.LogEntry");
if (logEntriesType != null && logEntryType != null)
{
// Get total count
var getCountMethod = logEntriesType.GetMethod("GetCount", BindingFlags.Static | BindingFlags.Public);
int totalCount = getCountMethod != null ? (int)getCountMethod.Invoke(null, null) : 0;
// Start from the most recent logs
int start = Math.Max(0, totalCount - param.count);
// Get entries
var getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public);
if (getEntryMethod != null)
{
for (int i = start; i < totalCount; i++)
{
var entry = Activator.CreateInstance(logEntryType);
var parameters = new object[] { i, entry };
getEntryMethod.Invoke(null, parameters);
// Extract fields
var messageField = logEntryType.GetField("message", BindingFlags.Public | BindingFlags.Instance);
var conditionField = logEntryType.GetField("condition", BindingFlags.Public | BindingFlags.Instance);
var modeField = logEntryType.GetField("mode", BindingFlags.Public | BindingFlags.Instance);
string message = conditionField != null ? (string)conditionField.GetValue(entry) : "";
string stackTrace = messageField != null ? (string)messageField.GetValue(entry) : "";
int mode = modeField != null ? (int)modeField.GetValue(entry) : 0;
// If message is empty, use first line of stackTrace as message
if (string.IsNullOrEmpty(message) && !string.IsNullOrEmpty(stackTrace))
{
int firstNewLine = stackTrace.IndexOf('\n');
if (firstNewLine > 0)
{
message = stackTrace.Substring(0, firstNewLine);
}
else
{
message = stackTrace;
}
}
// Convert mode to LogType
LogType logType = ConvertModeToLogType(mode);
// Filter by type
if (param.errorsOnly)
{
if (logType != LogType.Error && logType != LogType.Exception)
continue;
}
else if (!param.includeWarnings)
{
if (logType == LogType.Warning)
continue;
}
logs.Add(new ConsoleLogEntry
{
message = message,
stackTrace = stackTrace,
type = (int)logType,
timestamp = "" // Editor 로그는 실제 발생 시간을 알 수 없음
});
}
}
}
}
}
catch (Exception ex)
{
ToolkitLogger.LogWarning("ConsoleHandler", $"Failed to get Editor console logs: {ex.Message}");
// Fallback to runtime logs
lock (logLock)
{
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);
}
}
}
#else
// Runtime: use Application.logMessageReceived logs
lock (logLock)
{
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];
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);
}
}
#endif
return logs;
}
private LogType ConvertModeToLogType(int mode)
{
// Unity LogEntry mode flags
// Error = 1 << 0 = 1
// Assert = 1 << 1 = 2
// Log = 1 << 2 = 4
// Fatal = 1 << 4 = 16
// DontPreprocessCondition = 1 << 5 = 32
// AssetImportError = 1 << 6 = 64
// AssetImportWarning = 1 << 7 = 128
// ScriptingError = 1 << 8 = 256
// ScriptingWarning = 1 << 9 = 512
// ScriptingLog = 1 << 10 = 1024
// ScriptCompileError = 1 << 11 = 2048
// ScriptCompileWarning = 1 << 12 = 4096
// StickyError = 1 << 13 = 8192
// MayIgnoreLineNumber = 1 << 14 = 16384
// ReportBug = 1 << 15 = 32768
// DisplayPreviousErrorInStatusBar = 1 << 16 = 65536
// ScriptingException = 1 << 17 = 131072
// DontExtractStacktrace = 1 << 18 = 262144
// ShouldClearOnPlay = 1 << 19 = 524288
// GraphCompileError = 1 << 20 = 1048576
// ScriptingAssertion = 1 << 21 = 2097152
// Check error flags
if ((mode & (1 | 64 | 256 | 2048 | 1048576)) != 0)
return LogType.Error;
// Check exception flags
if ((mode & 131072) != 0)
return LogType.Exception;
// Check warning flags
if ((mode & (128 | 512 | 4096)) != 0)
return LogType.Warning;
// Check assert flags
if ((mode & (2 | 2097152)) != 0)
return LogType.Assert;
// Default to Log
return LogType.Log;
}
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
{
ToolkitLogger.LogWarning("ConsoleHandler", "LogEntries.Clear method not found");
}
}
else
{
ToolkitLogger.LogWarning("ConsoleHandler", "UnityEditor.LogEntries type not found");
}
}
}
catch (Exception ex)
{
ToolkitLogger.LogWarning("ConsoleHandler", $"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: 34faf809a052bfe4c968da086d1fa1de

View File

@@ -0,0 +1,806 @@
using System;
using System.IO;
using UnityEngine;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Database;
using UnityEditorToolkit.Editor.Utils;
using Cysharp.Threading.Tasks;
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Database command handler
/// SQLite 데이터베이스 관리 명령어
/// </summary>
public class DatabaseHandler : BaseHandler
{
public override string Category => "Database";
// Reset operation state tracking (for async reset via ResponseQueue)
private static bool resetInProgress = false;
private static OperationResult resetResult = null;
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Status":
return HandleStatus();
case "Connect":
return HandleConnect(request);
case "Disconnect":
return HandleDisconnect();
case "Reset":
return HandleReset(request);
case "RunMigrations":
return HandleRunMigrations();
case "ClearMigrations":
return HandleClearMigrations();
case "Undo":
return HandleUndo();
case "Redo":
return HandleRedo();
case "GetHistory":
return HandleGetHistory(request);
case "ClearHistory":
return HandleClearHistory();
case "Query":
return HandleQuery(request);
default:
throw new ArgumentException($"Unknown method: {method}");
}
}
#region Status
private object HandleStatus()
{
var manager = DatabaseManager.Instance;
var health = manager.GetHealthStatus();
return new DatabaseStatusResult
{
isInitialized = health.IsInitialized,
isConnected = health.IsConnected,
isEnabled = health.IsEnabled,
databaseFilePath = health.DatabaseFilePath,
databaseFileExists = health.DatabaseFileExists,
undoCount = manager.CommandHistory?.UndoCount ?? 0,
redoCount = manager.CommandHistory?.RedoCount ?? 0
};
}
#endregion
#region Connect
private class ConnectParams
{
public string databaseFilePath { get; set; }
public bool enableWAL { get; set; } = true;
}
private object HandleConnect(JsonRpcRequest request)
{
if (DatabaseManager.Instance.IsConnected)
{
return new OperationResult
{
success = true,
message = "Already connected"
};
}
var config = DatabaseConfig.LoadFromEditorPrefs();
// Override with request params if provided
if (request.Params != null)
{
var paramsObj = request.GetParams<ConnectParams>();
if (paramsObj != null)
{
if (!string.IsNullOrEmpty(paramsObj.databaseFilePath))
{
config.DatabaseFilePath = paramsObj.databaseFilePath;
}
config.EnableWAL = paramsObj.enableWAL;
}
}
// Synchronous wrapper (blocking call) - Convert UniTask to Task for synchronous execution
var result = DatabaseManager.Instance.InitializeAsync(config).AsTask().GetAwaiter().GetResult();
return new OperationResult
{
success = result.Success,
message = result.Success ? "Connected successfully" : result.ErrorMessage
};
}
#endregion
#region Disconnect
private object HandleDisconnect()
{
if (!DatabaseManager.Instance.IsConnected)
{
return new OperationResult
{
success = true,
message = "Not connected"
};
}
// Synchronous wrapper - Convert UniTask to Task for synchronous execution
DatabaseManager.Instance.ShutdownAsync().AsTask().GetAwaiter().GetResult();
return new OperationResult
{
success = true,
message = "Disconnected successfully"
};
}
#endregion
#region Reset
private object HandleReset(JsonRpcRequest request)
{
// Check if reset is already in progress
if (resetInProgress)
{
return new OperationResult
{
success = false,
message = "Reset operation already in progress"
};
}
// Get send callback from request context
var sendCallback = request.GetContext<Action<string>>("sendCallback");
if (sendCallback == null)
{
throw new Exception("Send callback not found in request context");
}
string requestId = request.Id?.ToString() ?? string.Empty;
// Start async reset operation
resetInProgress = true;
resetResult = null;
// Fire and forget async operation
ResetDatabaseAsync().Forget();
// Register delayed response
ResponseQueue.Instance.Register(
requestId,
condition: () => !resetInProgress,
resultProvider: () => resetResult ?? new OperationResult
{
success = false,
message = "Reset operation failed unexpectedly"
},
sendCallback,
timeoutSeconds: 120.0 // 2 minutes timeout
);
// Return null to indicate delayed response
return null;
}
private async UniTaskVoid ResetDatabaseAsync()
{
try
{
var config = DatabaseConfig.LoadFromEditorPrefs();
string dbPath = config.DatabaseFilePath;
ToolkitLogger.Log("DatabaseHandler", "Starting database reset...");
// Shutdown first (check IsInitialized, not just IsConnected)
if (DatabaseManager.Instance.IsInitialized)
{
try
{
ToolkitLogger.Log("DatabaseHandler", "Shutting down database...");
await DatabaseManager.Instance.ShutdownAsync();
ToolkitLogger.Log("DatabaseHandler", "Database shutdown complete.");
}
catch (Exception ex)
{
ToolkitLogger.LogWarning("DatabaseHandler", $"Shutdown warning: {ex.Message}");
}
}
// Delete database file
bool fileDeleted = false;
if (File.Exists(dbPath))
{
try
{
File.Delete(dbPath);
fileDeleted = true;
ToolkitLogger.Log("DatabaseHandler", $"Database file deleted: {dbPath}");
}
catch (Exception ex)
{
resetResult = new OperationResult
{
success = false,
message = $"Failed to delete database file: {ex.Message}"
};
resetInProgress = false;
return;
}
}
// Reconnect (will run migrations automatically)
ToolkitLogger.Log("DatabaseHandler", "Reconnecting to database...");
var result = await DatabaseManager.Instance.InitializeAsync(config);
resetResult = new OperationResult
{
success = result.Success,
message = result.Success
? $"Database reset successfully. File deleted: {fileDeleted}"
: $"Reset failed: {result.ErrorMessage}"
};
ToolkitLogger.Log("DatabaseHandler", $"Reset complete: {resetResult.message}");
}
catch (Exception ex)
{
ToolkitLogger.LogError("DatabaseHandler", $"Reset exception: {ex.Message}");
resetResult = new OperationResult
{
success = false,
message = $"Reset failed: {ex.Message}"
};
}
finally
{
resetInProgress = false;
}
}
#endregion
#region RunMigrations
private object HandleRunMigrations()
{
if (!DatabaseManager.Instance.IsConnected)
{
return new OperationResult
{
success = false,
message = "Not connected to database"
};
}
var runner = new MigrationRunner(DatabaseManager.Instance);
var result = runner.RunMigrationsAsync().AsTask().GetAwaiter().GetResult();
return new MigrationOperationResult
{
success = result.Success,
message = result.Success
? $"Migrations completed: {result.MigrationsApplied} applied"
: result.ErrorMessage,
migrationsApplied = result.MigrationsApplied
};
}
#endregion
#region ClearMigrations
private object HandleClearMigrations()
{
if (!DatabaseManager.Instance.IsConnected)
{
return new OperationResult
{
success = false,
message = "Not connected to database"
};
}
try
{
var connection = DatabaseManager.Instance.Connector.Connection;
int deleted = connection.Execute("DELETE FROM migrations");
return new OperationResult
{
success = true,
message = $"Cleared {deleted} migration record(s)"
};
}
catch (Exception ex)
{
return new OperationResult
{
success = false,
message = $"Failed to clear migrations: {ex.Message}"
};
}
}
#endregion
#region Undo
private object HandleUndo()
{
if (!DatabaseManager.Instance.IsConnected)
{
return new UndoRedoResult
{
success = false,
message = "Not connected to database",
commandName = "",
remainingUndo = 0,
remainingRedo = 0
};
}
var history = DatabaseManager.Instance.CommandHistory;
if (history == null || history.UndoCount == 0)
{
return new UndoRedoResult
{
success = false,
message = "Nothing to undo",
commandName = "",
remainingUndo = 0,
remainingRedo = history?.RedoCount ?? 0
};
}
try
{
string commandName = history.PeekUndo()?.CommandName ?? "Unknown";
bool result = history.Undo();
return new UndoRedoResult
{
success = result,
message = result ? "Undo successful" : "Undo failed",
commandName = commandName,
remainingUndo = history.UndoCount,
remainingRedo = history.RedoCount
};
}
catch (Exception ex)
{
return new UndoRedoResult
{
success = false,
message = $"Undo failed: {ex.Message}",
commandName = "",
remainingUndo = history.UndoCount,
remainingRedo = history.RedoCount
};
}
}
#endregion
#region Redo
private object HandleRedo()
{
if (!DatabaseManager.Instance.IsConnected)
{
return new UndoRedoResult
{
success = false,
message = "Not connected to database",
commandName = "",
remainingUndo = 0,
remainingRedo = 0
};
}
var history = DatabaseManager.Instance.CommandHistory;
if (history == null || history.RedoCount == 0)
{
return new UndoRedoResult
{
success = false,
message = "Nothing to redo",
commandName = "",
remainingUndo = history?.UndoCount ?? 0,
remainingRedo = 0
};
}
try
{
string commandName = history.PeekRedo()?.CommandName ?? "Unknown";
bool result = history.Redo();
return new UndoRedoResult
{
success = result,
message = result ? "Redo successful" : "Redo failed",
commandName = commandName,
remainingUndo = history.UndoCount,
remainingRedo = history.RedoCount
};
}
catch (Exception ex)
{
return new UndoRedoResult
{
success = false,
message = $"Redo failed: {ex.Message}",
commandName = "",
remainingUndo = history.UndoCount,
remainingRedo = history.RedoCount
};
}
}
#endregion
#region GetHistory
private class GetHistoryParams
{
public int limit { get; set; } = 10;
}
private object HandleGetHistory(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
return new HistoryResult
{
undoStack = new HistoryEntryResult[0],
redoStack = new HistoryEntryResult[0],
totalUndo = 0,
totalRedo = 0
};
}
var history = DatabaseManager.Instance.CommandHistory;
if (history == null)
{
return new HistoryResult
{
undoStack = new HistoryEntryResult[0],
redoStack = new HistoryEntryResult[0],
totalUndo = 0,
totalRedo = 0
};
}
int limit = 10;
if (request.Params != null)
{
var paramsObj = request.GetParams<GetHistoryParams>();
if (paramsObj != null)
{
limit = paramsObj.limit;
}
}
var undoCommands = history.GetUndoStack(limit);
var redoCommands = history.GetRedoStack(limit);
var undoEntries = new HistoryEntryResult[undoCommands.Count];
for (int i = 0; i < undoCommands.Count; i++)
{
var cmd = undoCommands[i];
undoEntries[i] = new HistoryEntryResult
{
name = cmd.CommandName,
timestamp = cmd.ExecutedAt.ToString("yyyy-MM-dd HH:mm:ss"),
canUndo = true
};
}
var redoEntries = new HistoryEntryResult[redoCommands.Count];
for (int i = 0; i < redoCommands.Count; i++)
{
var cmd = redoCommands[i];
redoEntries[i] = new HistoryEntryResult
{
name = cmd.CommandName,
timestamp = cmd.ExecutedAt.ToString("yyyy-MM-dd HH:mm:ss"),
canUndo = false
};
}
return new HistoryResult
{
undoStack = undoEntries,
redoStack = redoEntries,
totalUndo = history.UndoCount,
totalRedo = history.RedoCount
};
}
#endregion
#region ClearHistory
private object HandleClearHistory()
{
if (!DatabaseManager.Instance.IsConnected)
{
return new OperationResult
{
success = false,
message = "Not connected to database"
};
}
var history = DatabaseManager.Instance.CommandHistory;
if (history == null)
{
return new OperationResult
{
success = false,
message = "Command history not available"
};
}
try
{
int undoCount = history.UndoCount;
int redoCount = history.RedoCount;
history.Clear();
return new OperationResult
{
success = true,
message = $"Cleared {undoCount} undo and {redoCount} redo entries"
};
}
catch (Exception ex)
{
return new OperationResult
{
success = false,
message = $"Failed to clear history: {ex.Message}"
};
}
}
#endregion
#region Query
private class QueryParams
{
public string table { get; set; }
public int limit { get; set; } = 100;
}
// Pre-defined table schemas for safe querying
private class MigrationRecord
{
public int migration_id { get; set; }
public string migration_name { get; set; }
public string applied_at { get; set; }
}
private class CommandHistoryQueryRecord
{
public string command_id { get; set; }
public string command_name { get; set; }
public string command_type { get; set; }
public string command_data { get; set; }
public string executed_at { get; set; }
public string executed_by { get; set; }
}
private class TransformQueryRecord
{
public int transform_id { get; set; }
public int object_id { get; set; }
public float position_x { get; set; }
public float position_y { get; set; }
public float position_z { get; set; }
public float rotation_x { get; set; }
public float rotation_y { get; set; }
public float rotation_z { get; set; }
public float rotation_w { get; set; }
public float scale_x { get; set; }
public float scale_y { get; set; }
public float scale_z { get; set; }
public string recorded_at { get; set; }
}
private object HandleQuery(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
return new QueryResult
{
success = false,
message = "Not connected to database",
rows = new object[0],
columns = new string[0],
rowCount = 0
};
}
if (request.Params == null)
{
return new QueryResult
{
success = false,
message = "Table name is required. Supported: migrations, command_history, transforms",
rows = new object[0],
columns = new string[0],
rowCount = 0
};
}
var paramsObj = request.GetParams<QueryParams>();
if (paramsObj == null || string.IsNullOrEmpty(paramsObj.table))
{
return new QueryResult
{
success = false,
message = "Table name is required. Supported: migrations, command_history, transforms",
rows = new object[0],
columns = new string[0],
rowCount = 0
};
}
try
{
var connection = DatabaseManager.Instance.Connector.Connection;
var results = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>();
string[] columnNames = null;
string tableName = paramsObj.table.ToLower().Trim();
switch (tableName)
{
case "migrations":
{
columnNames = new[] { "migration_id", "migration_name", "applied_at" };
string sql = $"SELECT migration_id, migration_name, applied_at FROM migrations ORDER BY migration_id DESC LIMIT {paramsObj.limit}";
var records = connection.Query<MigrationRecord>(sql);
foreach (var record in records)
{
var row = new System.Collections.Generic.Dictionary<string, object>
{
["migration_id"] = record.migration_id,
["migration_name"] = record.migration_name,
["applied_at"] = record.applied_at
};
results.Add(row);
}
}
break;
case "command_history":
{
columnNames = new[] { "command_id", "command_name", "command_type", "command_data", "executed_at", "executed_by" };
string sql = $"SELECT command_id, command_name, command_type, command_data, executed_at, executed_by FROM command_history ORDER BY executed_at DESC LIMIT {paramsObj.limit}";
var records = connection.Query<CommandHistoryQueryRecord>(sql);
foreach (var record in records)
{
var row = new System.Collections.Generic.Dictionary<string, object>
{
["command_id"] = record.command_id,
["command_name"] = record.command_name,
["command_type"] = record.command_type,
["command_data"] = record.command_data,
["executed_at"] = record.executed_at,
["executed_by"] = record.executed_by
};
results.Add(row);
}
}
break;
case "transforms":
{
columnNames = new[] { "transform_id", "object_id", "position_x", "position_y", "position_z", "rotation_x", "rotation_y", "rotation_z", "rotation_w", "scale_x", "scale_y", "scale_z", "recorded_at" };
string sql = $"SELECT transform_id, object_id, position_x, position_y, position_z, rotation_x, rotation_y, rotation_z, rotation_w, scale_x, scale_y, scale_z, recorded_at FROM transforms ORDER BY recorded_at DESC LIMIT {paramsObj.limit}";
var records = connection.Query<TransformQueryRecord>(sql);
foreach (var record in records)
{
var row = new System.Collections.Generic.Dictionary<string, object>
{
["transform_id"] = record.transform_id,
["object_id"] = record.object_id,
["position_x"] = record.position_x,
["position_y"] = record.position_y,
["position_z"] = record.position_z,
["rotation_x"] = record.rotation_x,
["rotation_y"] = record.rotation_y,
["rotation_z"] = record.rotation_z,
["rotation_w"] = record.rotation_w,
["scale_x"] = record.scale_x,
["scale_y"] = record.scale_y,
["scale_z"] = record.scale_z,
["recorded_at"] = record.recorded_at
};
results.Add(row);
}
}
break;
default:
return new QueryResult
{
success = false,
message = $"Unknown table: {paramsObj.table}. Supported: migrations, command_history, transforms",
rows = new object[0],
columns = new string[0],
rowCount = 0
};
}
return new QueryResult
{
success = true,
message = $"Query executed successfully. {results.Count} row(s) returned.",
rows = results.ToArray(),
columns = columnNames ?? new string[0],
rowCount = results.Count
};
}
catch (Exception ex)
{
return new QueryResult
{
success = false,
message = $"Query failed: {ex.Message}",
rows = new object[0],
columns = new string[0],
rowCount = 0
};
}
}
#endregion
}
#region Response Types
public class DatabaseStatusResult
{
public bool isInitialized { get; set; }
public bool isConnected { get; set; }
public bool isEnabled { get; set; }
public string databaseFilePath { get; set; }
public bool databaseFileExists { get; set; }
public int undoCount { get; set; }
public int redoCount { get; set; }
}
public class OperationResult
{
public bool success { get; set; }
public string message { get; set; }
}
public class MigrationOperationResult : OperationResult
{
public int migrationsApplied { get; set; }
}
public class UndoRedoResult : OperationResult
{
public string commandName { get; set; }
public int remainingUndo { get; set; }
public int remainingRedo { get; set; }
}
public class HistoryEntryResult
{
public string name { get; set; }
public string timestamp { get; set; }
public bool canUndo { get; set; }
}
public class HistoryResult
{
public HistoryEntryResult[] undoStack { get; set; }
public HistoryEntryResult[] redoStack { get; set; }
public int totalUndo { get; set; }
public int totalRedo { get; set; }
}
public class QueryResult : OperationResult
{
public object[] rows { get; set; }
public string[] columns { get; set; }
public int rowCount { get; set; }
}
#endregion
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 38fbb8a2eff8cc9bee6e8536e40d5062

View File

@@ -0,0 +1,427 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Attributes;
using UnityEditorToolkit.Editor.Utils;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Editor utility commands
/// </summary>
public class EditorHandler : BaseHandler
{
public override string Category => "Editor";
private static Dictionary<string, MethodInfo> executableMethods;
private static bool isInitialized = false;
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Refresh":
return HandleRefresh(request);
case "Recompile":
return HandleRecompile(request);
case "Reimport":
return HandleReimport(request);
case "GetSelection":
return HandleGetSelection(request);
case "SetSelection":
return HandleSetSelection(request);
case "FocusGameView":
return HandleFocusGameView(request);
case "FocusSceneView":
return HandleFocusSceneView(request);
case "Execute":
return HandleExecute(request);
case "ListExecutable":
return HandleListExecutable(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
private object HandleRefresh(JsonRpcRequest request)
{
#if UNITY_EDITOR
try
{
AssetDatabase.Refresh();
return new { success = true, message = "AssetDatabase refreshed" };
}
catch (Exception ex)
{
throw new Exception($"Failed to refresh AssetDatabase: {ex.Message}");
}
#else
throw new Exception("Refresh is only available in Unity Editor");
#endif
}
private object HandleRecompile(JsonRpcRequest request)
{
#if UNITY_EDITOR
try
{
// Request script compilation
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
return new { success = true, message = "Script recompilation requested" };
}
catch (Exception ex)
{
throw new Exception($"Failed to request recompilation: {ex.Message}");
}
#else
throw new Exception("Recompile is only available in Unity Editor");
#endif
}
private object HandleReimport(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<ReimportParams>(request, "path");
try
{
// Build Unity virtual path and physical path
string assetPath = $"Assets/{param.path}";
string physicalPath = System.IO.Path.Combine(Application.dataPath, param.path);
// Validate path exists using physical path
if (!System.IO.File.Exists(physicalPath) && !System.IO.Directory.Exists(physicalPath))
{
throw new Exception($"Asset not found: {assetPath}");
}
// Reimport the asset using Unity virtual path
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
return new { success = true, path = assetPath, message = "Asset reimported" };
}
catch (Exception ex)
{
throw new Exception($"Failed to reimport asset: {ex.Message}");
}
#else
throw new Exception("Reimport is only available in Unity Editor");
#endif
}
private object HandleGetSelection(JsonRpcRequest request)
{
#if UNITY_EDITOR
var activeObject = Selection.activeGameObject;
var selectedObjects = Selection.gameObjects;
if (selectedObjects == null || selectedObjects.Length == 0)
{
return new
{
success = true,
count = 0,
activeObject = (object)null,
selection = new object[0]
};
}
var selectionList = selectedObjects.Select(obj => new
{
name = obj.name,
instanceId = obj.GetInstanceID(),
type = obj.GetType().Name
}).ToList();
return new
{
success = true,
count = selectedObjects.Length,
activeObject = activeObject != null ? new
{
name = activeObject.name,
instanceId = activeObject.GetInstanceID(),
type = activeObject.GetType().Name
} : null,
selection = selectionList
};
#else
throw new Exception("GetSelection is only available in Unity Editor");
#endif
}
private object HandleSetSelection(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<SetSelectionParams>(request, "names");
try
{
var selectedObjects = new List<GameObject>();
var selectedNames = new List<string>();
foreach (var nameOrPath in param.names)
{
GameObject obj = null;
// Try finding by path first (e.g., "Parent/Child/Target")
if (nameOrPath.Contains("/"))
{
obj = GameObject.Find(nameOrPath);
}
// Try finding by name in all scene objects
if (obj == null)
{
var allObjects = UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None);
obj = allObjects.FirstOrDefault(go => go.name == nameOrPath);
}
if (obj != null)
{
selectedObjects.Add(obj);
selectedNames.Add(obj.name);
}
}
if (selectedObjects.Count > 0)
{
Selection.objects = selectedObjects.ToArray();
}
else
{
Selection.objects = new UnityEngine.Object[0];
}
return new
{
success = true,
selectedCount = selectedObjects.Count,
selectedNames = selectedNames
};
}
catch (Exception ex)
{
throw new Exception($"Failed to set selection: {ex.Message}");
}
#else
throw new Exception("SetSelection is only available in Unity Editor");
#endif
}
private object HandleFocusGameView(JsonRpcRequest request)
{
#if UNITY_EDITOR
try
{
EditorApplication.ExecuteMenuItem("Window/General/Game");
return new { success = true, message = "Game View focused" };
}
catch (Exception ex)
{
throw new Exception($"Failed to focus Game View: {ex.Message}");
}
#else
throw new Exception("FocusGameView is only available in Unity Editor");
#endif
}
private object HandleFocusSceneView(JsonRpcRequest request)
{
#if UNITY_EDITOR
try
{
EditorApplication.ExecuteMenuItem("Window/General/Scene");
return new { success = true, message = "Scene View focused" };
}
catch (Exception ex)
{
throw new Exception($"Failed to focus Scene View: {ex.Message}");
}
#else
throw new Exception("FocusSceneView is only available in Unity Editor");
#endif
}
#if UNITY_EDITOR
private string GetGameObjectPath(GameObject obj)
{
string path = obj.name;
Transform parent = obj.transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
#endif
private void InitializeExecutableMethods()
{
if (isInitialized)
return;
executableMethods = new Dictionary<string, MethodInfo>();
try
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
try
{
var types = assembly.GetTypes();
foreach (var type in types)
{
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
foreach (var method in methods)
{
var attribute = method.GetCustomAttribute<ExecutableMethodAttribute>();
if (attribute != null)
{
if (!method.IsStatic)
{
ToolkitLogger.LogWarning("EditorHandler", $"Method {type.FullName}.{method.Name} has [ExecutableMethod] but is not static. Skipping.");
continue;
}
if (method.ReturnType != typeof(void))
{
ToolkitLogger.LogWarning("EditorHandler", $"Method {type.FullName}.{method.Name} has [ExecutableMethod] but does not return void. Skipping.");
continue;
}
if (method.GetParameters().Length > 0)
{
ToolkitLogger.LogWarning("EditorHandler", $"Method {type.FullName}.{method.Name} has [ExecutableMethod] but has parameters. Skipping.");
continue;
}
if (executableMethods.ContainsKey(attribute.CommandName))
{
ToolkitLogger.LogWarning("EditorHandler", $"Duplicate command name '{attribute.CommandName}'. Method {type.FullName}.{method.Name} will override previous registration.");
}
executableMethods[attribute.CommandName] = method;
ToolkitLogger.LogDebug("EditorHandler", $"Registered executable method: '{attribute.CommandName}' -> {type.FullName}.{method.Name}");
}
}
}
}
catch (Exception ex)
{
ToolkitLogger.LogWarning("EditorHandler", $"Failed to scan assembly {assembly.FullName}: {ex.Message}");
}
}
ToolkitLogger.LogDebug("EditorHandler", $"Initialized with {executableMethods.Count} executable methods");
isInitialized = true;
}
catch (Exception ex)
{
ToolkitLogger.LogError("EditorHandler", $"Failed to initialize executable methods: {ex.Message}");
executableMethods = new Dictionary<string, MethodInfo>();
isInitialized = true;
}
}
private object HandleExecute(JsonRpcRequest request)
{
InitializeExecutableMethods();
var param = ValidateParam<ExecuteParams>(request, "commandName");
if (string.IsNullOrWhiteSpace(param.commandName))
{
throw new Exception("Command name is required");
}
if (!executableMethods.TryGetValue(param.commandName, out var methodInfo))
{
throw new Exception($"Unknown command: '{param.commandName}'. Use Editor.ListExecutable to see available commands.");
}
try
{
ToolkitLogger.Log("EditorHandler", $"Executing command: '{param.commandName}'");
methodInfo.Invoke(null, null);
return new
{
success = true,
commandName = param.commandName,
message = $"Command '{param.commandName}' executed successfully"
};
}
catch (TargetInvocationException ex)
{
var innerException = ex.InnerException ?? ex;
ToolkitLogger.LogError("EditorHandler", $"Failed to execute '{param.commandName}': {innerException.Message}\n{innerException.StackTrace}");
throw new Exception($"Failed to execute '{param.commandName}': {innerException.Message}");
}
catch (Exception ex)
{
ToolkitLogger.LogError("EditorHandler", $"Failed to execute '{param.commandName}': {ex.Message}\n{ex.StackTrace}");
throw new Exception($"Failed to execute '{param.commandName}': {ex.Message}");
}
}
private object HandleListExecutable(JsonRpcRequest request)
{
InitializeExecutableMethods();
var methods = executableMethods.Select(kvp =>
{
var methodInfo = kvp.Value;
var attribute = methodInfo.GetCustomAttribute<ExecutableMethodAttribute>();
return new
{
commandName = kvp.Key,
description = attribute?.Description ?? "",
className = methodInfo.DeclaringType?.FullName ?? "Unknown",
methodName = methodInfo.Name
};
}).OrderBy(m => m.commandName).ToList();
return new
{
success = true,
count = methods.Count,
methods = methods
};
}
// Parameter classes
[Serializable]
public class ReimportParams
{
public string path;
}
[Serializable]
public class SetSelectionParams
{
public string[] names;
}
[Serializable]
public class ExecuteParams
{
public string commandName;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3ed404892bdf87644915acb29004a870

View File

@@ -0,0 +1,469 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Database;
using UnityEditorToolkit.Editor.Database.Commands;
using Cysharp.Threading.Tasks;
using UnityEditorToolkit.Editor.Utils;
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);
case "SetParent":
return HandleSetParent(request);
case "GetParent":
return HandleGetParent(request);
case "GetChildren":
return HandleGetChildren(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");
// Find parent GameObject if specified
GameObject parentObj = null;
if (!string.IsNullOrEmpty(param.parent))
{
parentObj = FindGameObject(param.parent);
if (parentObj == null)
{
throw new Exception($"Parent GameObject not found: {param.parent}");
}
}
GameObject obj = new GameObject(param.name);
// Set parent if specified
if (parentObj != null)
{
obj.transform.SetParent(parentObj.transform);
}
// Register undo
#if UNITY_EDITOR
UnityEditor.Undo.RegisterCreatedObjectUndo(obj, "Create GameObject");
#endif
// Record Command for history (without re-executing creation)
RecordCreateCommandAsync(obj, parentObj).Forget();
return new GameObjectInfo
{
name = obj.name,
instanceId = obj.GetInstanceID(),
path = GetGameObjectPath(obj),
active = obj.activeSelf,
tag = obj.tag,
layer = obj.layer
};
}
/// <summary>
/// Record CreateGameObjectCommand for history (without re-executing)
/// </summary>
private async UniTaskVoid RecordCreateCommandAsync(GameObject obj, GameObject parent)
{
try
{
#if UNITY_EDITOR
// Check if database is connected
if (DatabaseManager.Instance == null ||
!DatabaseManager.Instance.IsConnected ||
DatabaseManager.Instance.CommandHistory == null)
{
return;
}
// Create command with already created GameObject reference
var command = CreateGameObjectCommand.CreateFromExisting(obj, parent);
// Add to history without executing (already created)
await DatabaseManager.Instance.CommandHistory.RecordCommandAsync(command);
#endif
}
catch (Exception ex)
{
ToolkitLogger.LogWarning("GameObjectHandler", $"Command recording failed: {ex.Message}");
}
}
/// <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}");
}
// Execute Command Pattern before actual destruction (database persistence)
ExecuteDeleteCommandAsync(obj).Forget();
#if UNITY_EDITOR
UnityEditor.Undo.DestroyObjectImmediate(obj);
#else
GameObject.DestroyImmediate(obj);
#endif
return new { success = true };
}
/// <summary>
/// Execute DeleteGameObjectCommand asynchronously (database persistence)
/// </summary>
private async UniTaskVoid ExecuteDeleteCommandAsync(GameObject obj)
{
try
{
#if UNITY_EDITOR
// Check if database is connected
if (DatabaseManager.Instance == null ||
!DatabaseManager.Instance.IsConnected ||
DatabaseManager.Instance.CommandHistory == null)
{
return;
}
// Create command
var command = new DeleteGameObjectCommand(obj);
// Execute through CommandHistory (async, database persistence)
// Note: DeleteGameObjectCommand.CanPersist = false (GameObject reference)
// So it will be added to Undo stack but not persisted to database
await DatabaseManager.Instance.CommandHistory.ExecuteCommandAsync(command);
#endif
}
catch (Exception ex)
{
ToolkitLogger.LogWarning("GameObjectHandler", $"Command execution failed: {ex.Message}");
}
}
/// <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 };
}
/// <summary>
/// Set or remove parent of GameObject
/// </summary>
private object HandleSetParent(JsonRpcRequest request)
{
var param = ValidateParam<SetParentParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
#if UNITY_EDITOR
UnityEditor.Undo.SetTransformParent(obj.transform, null, "Set Parent");
#endif
// parent가 null이거나 빈 문자열이면 부모 제거
if (string.IsNullOrEmpty(param.parent))
{
obj.transform.SetParent(null, param.worldPositionStays);
return new SetParentResult
{
success = true,
name = obj.name,
parent = null,
path = GetGameObjectPath(obj)
};
}
// 새 부모 찾기
var newParent = FindGameObject(param.parent);
if (newParent == null)
{
throw new Exception($"Parent GameObject not found: {param.parent}");
}
// 순환 참조 체크
if (IsDescendantOf(newParent.transform, obj.transform))
{
throw new Exception($"Cannot set parent: {param.parent} is a descendant of {param.name}");
}
#if UNITY_EDITOR
UnityEditor.Undo.SetTransformParent(obj.transform, newParent.transform, "Set Parent");
#endif
obj.transform.SetParent(newParent.transform, param.worldPositionStays);
return new SetParentResult
{
success = true,
name = obj.name,
parent = newParent.name,
path = GetGameObjectPath(obj)
};
}
/// <summary>
/// Get parent information of GameObject
/// </summary>
private object HandleGetParent(JsonRpcRequest request)
{
var param = ValidateParam<FindParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
var parent = obj.transform.parent;
if (parent == null)
{
return new ParentInfo
{
hasParent = false,
parent = null
};
}
return new ParentInfo
{
hasParent = true,
parent = new GameObjectInfo
{
name = parent.gameObject.name,
instanceId = parent.gameObject.GetInstanceID(),
path = GetGameObjectPath(parent.gameObject),
active = parent.gameObject.activeSelf,
tag = parent.gameObject.tag,
layer = parent.gameObject.layer
}
};
}
/// <summary>
/// Get children of GameObject
/// </summary>
private object HandleGetChildren(JsonRpcRequest request)
{
var param = ValidateParam<GetChildrenParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
var children = new List<GameObjectInfo>();
var transform = obj.transform;
// recursive 옵션에 따라 직접 자식만 또는 모든 자손 반환
if (param.recursive)
{
GetAllDescendants(transform, children);
}
else
{
for (int i = 0; i < transform.childCount; i++)
{
var child = transform.GetChild(i).gameObject;
children.Add(new GameObjectInfo
{
name = child.name,
instanceId = child.GetInstanceID(),
path = GetGameObjectPath(child),
active = child.activeSelf,
tag = child.tag,
layer = child.layer
});
}
}
return new ChildrenInfo
{
count = children.Count,
children = children
};
}
/// <summary>
/// 재귀적으로 모든 자손 가져오기
/// </summary>
private void GetAllDescendants(Transform parent, List<GameObjectInfo> list)
{
for (int i = 0; i < parent.childCount; i++)
{
var child = parent.GetChild(i).gameObject;
list.Add(new GameObjectInfo
{
name = child.name,
instanceId = child.GetInstanceID(),
path = GetGameObjectPath(child),
active = child.activeSelf,
tag = child.tag,
layer = child.layer
});
// 재귀 호출
GetAllDescendants(child.transform, list);
}
}
/// <summary>
/// target이 parent의 자손인지 확인 (순환 참조 방지)
/// </summary>
private bool IsDescendantOf(Transform target, Transform parent)
{
var current = target;
while (current != null)
{
if (current == parent)
return true;
current = current.parent;
}
return false;
}
// 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;
}
[Serializable]
private class SetParentParams
{
public string name;
public string parent; // null 또는 빈 문자열이면 부모 제거
public bool worldPositionStays = true; // 월드 좌표 유지 여부
}
[Serializable]
private class GetChildrenParams
{
public string name;
public bool recursive = false; // true면 모든 자손, false면 직접 자식만
}
// Response classes
[Serializable]
public class GameObjectInfo
{
public string name;
public int instanceId;
public string path;
public bool active;
public string tag;
public int layer;
}
[Serializable]
public class SetParentResult
{
public bool success;
public string name;
public string parent;
public string path;
}
[Serializable]
public class ParentInfo
{
public bool hasParent;
public GameObjectInfo parent;
}
[Serializable]
public class ChildrenInfo
{
public int count;
public List<GameObjectInfo> children;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 28de8e44386c3eb4085f117c77e4e4a1

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: c9ef397f870825f4dbeae7e46bc95981

View File

@@ -0,0 +1,618 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditorToolkit.Protocol;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Material commands
/// </summary>
public class MaterialHandler : BaseHandler
{
public override string Category => "Material";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "GetProperty":
return HandleGetProperty(request);
case "SetProperty":
return HandleSetProperty(request);
case "GetColor":
return HandleGetColor(request);
case "SetColor":
return HandleSetColor(request);
case "List":
return HandleList(request);
case "GetShader":
return HandleGetShader(request);
case "SetShader":
return HandleSetShader(request);
case "GetTexture":
return HandleGetTexture(request);
case "SetTexture":
return HandleSetTexture(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
private object HandleGetProperty(JsonRpcRequest request)
{
var param = ValidateParam<MaterialPropertyParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
throw new Exception($"No Renderer component found on: {param.gameObject}");
}
var material = GetMaterial(renderer, param.materialIndex, param.useShared);
if (!material.HasProperty(param.propertyName))
{
throw new Exception($"Material does not have property: {param.propertyName}");
}
object value = null;
string propertyType = GetPropertyType(material, param.propertyName);
switch (propertyType)
{
case "Float":
case "Range":
value = material.GetFloat(param.propertyName);
break;
case "Int":
value = material.GetInt(param.propertyName);
break;
case "Color":
var color = material.GetColor(param.propertyName);
value = new { r = color.r, g = color.g, b = color.b, a = color.a };
break;
case "Vector":
var vec = material.GetVector(param.propertyName);
value = new { x = vec.x, y = vec.y, z = vec.z, w = vec.w };
break;
case "Texture":
var tex = material.GetTexture(param.propertyName);
value = tex != null ? new { name = tex.name, type = tex.GetType().Name } : null;
break;
default:
value = "Unknown type";
break;
}
return new
{
success = true,
gameObject = param.gameObject,
material = material.name,
propertyName = param.propertyName,
propertyType = propertyType,
value = value
};
}
private object HandleSetProperty(JsonRpcRequest request)
{
var param = ValidateParam<MaterialSetPropertyParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
throw new Exception($"No Renderer component found on: {param.gameObject}");
}
var material = GetMaterial(renderer, param.materialIndex, param.useShared);
if (!material.HasProperty(param.propertyName))
{
throw new Exception($"Material does not have property: {param.propertyName}");
}
string propertyType = GetPropertyType(material, param.propertyName);
switch (propertyType)
{
case "Float":
case "Range":
material.SetFloat(param.propertyName, Convert.ToSingle(param.value));
break;
case "Int":
material.SetInt(param.propertyName, Convert.ToInt32(param.value));
break;
default:
throw new Exception($"Property type {propertyType} cannot be set with SetProperty. Use SetColor, SetTexture, or SetVector.");
}
#if UNITY_EDITOR
EditorUtility.SetDirty(material);
#endif
return new
{
success = true,
gameObject = param.gameObject,
material = material.name,
propertyName = param.propertyName,
propertyType = propertyType,
value = param.value
};
}
private object HandleGetColor(JsonRpcRequest request)
{
var param = ValidateParam<MaterialColorParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
throw new Exception($"No Renderer component found on: {param.gameObject}");
}
var material = GetMaterial(renderer, param.materialIndex, param.useShared);
string propName = string.IsNullOrEmpty(param.propertyName) ? "_Color" : param.propertyName;
if (!material.HasProperty(propName))
{
throw new Exception($"Material does not have color property: {propName}");
}
var color = material.GetColor(propName);
return new
{
success = true,
gameObject = param.gameObject,
material = material.name,
propertyName = propName,
color = new
{
r = color.r,
g = color.g,
b = color.b,
a = color.a,
hex = ColorUtility.ToHtmlStringRGBA(color)
}
};
}
private object HandleSetColor(JsonRpcRequest request)
{
var param = ValidateParam<MaterialSetColorParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
throw new Exception($"No Renderer component found on: {param.gameObject}");
}
var material = GetMaterial(renderer, param.materialIndex, param.useShared);
string propName = string.IsNullOrEmpty(param.propertyName) ? "_Color" : param.propertyName;
if (!material.HasProperty(propName))
{
throw new Exception($"Material does not have color property: {propName}");
}
Color newColor;
// Parse color from various formats
if (!string.IsNullOrEmpty(param.hex))
{
// Hex format: #RRGGBB or #RRGGBBAA
string hexValue = param.hex.StartsWith("#") ? param.hex : "#" + param.hex;
if (!ColorUtility.TryParseHtmlString(hexValue, out newColor))
{
throw new Exception($"Invalid hex color format: {param.hex}");
}
}
else
{
// RGBA format
newColor = new Color(
param.r ?? 1f,
param.g ?? 1f,
param.b ?? 1f,
param.a ?? 1f
);
}
material.SetColor(propName, newColor);
#if UNITY_EDITOR
EditorUtility.SetDirty(material);
#endif
return new
{
success = true,
gameObject = param.gameObject,
material = material.name,
propertyName = propName,
color = new
{
r = newColor.r,
g = newColor.g,
b = newColor.b,
a = newColor.a,
hex = ColorUtility.ToHtmlStringRGBA(newColor)
}
};
}
private object HandleList(JsonRpcRequest request)
{
var param = ValidateParam<MaterialListParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
throw new Exception($"No Renderer component found on: {param.gameObject}");
}
var materials = param.useShared ? renderer.sharedMaterials : renderer.materials;
var materialList = new List<object>();
for (int i = 0; i < materials.Length; i++)
{
var mat = materials[i];
if (mat != null)
{
materialList.Add(new
{
index = i,
name = mat.name,
shader = mat.shader != null ? mat.shader.name : "None"
});
}
}
return new
{
success = true,
gameObject = param.gameObject,
count = materialList.Count,
materials = materialList
};
}
private object HandleGetShader(JsonRpcRequest request)
{
var param = ValidateParam<MaterialBaseParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
throw new Exception($"No Renderer component found on: {param.gameObject}");
}
var material = GetMaterial(renderer, param.materialIndex, param.useShared);
var shader = material.shader;
return new
{
success = true,
gameObject = param.gameObject,
material = material.name,
shader = shader != null ? new
{
name = shader.name,
propertyCount = shader.GetPropertyCount()
} : null
};
}
private object HandleSetShader(JsonRpcRequest request)
{
var param = ValidateParam<MaterialSetShaderParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
throw new Exception($"No Renderer component found on: {param.gameObject}");
}
var material = GetMaterial(renderer, param.materialIndex, param.useShared);
var shader = Shader.Find(param.shaderName);
if (shader == null)
{
throw new Exception($"Shader not found: {param.shaderName}");
}
material.shader = shader;
#if UNITY_EDITOR
EditorUtility.SetDirty(material);
#endif
return new
{
success = true,
gameObject = param.gameObject,
material = material.name,
shader = shader.name
};
}
private object HandleGetTexture(JsonRpcRequest request)
{
var param = ValidateParam<MaterialTextureParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
throw new Exception($"No Renderer component found on: {param.gameObject}");
}
var material = GetMaterial(renderer, param.materialIndex, param.useShared);
string propName = string.IsNullOrEmpty(param.propertyName) ? "_MainTex" : param.propertyName;
if (!material.HasProperty(propName))
{
throw new Exception($"Material does not have texture property: {propName}");
}
var texture = material.GetTexture(propName);
var scale = material.GetTextureScale(propName);
var offset = material.GetTextureOffset(propName);
return new
{
success = true,
gameObject = param.gameObject,
material = material.name,
propertyName = propName,
texture = texture != null ? new
{
name = texture.name,
type = texture.GetType().Name,
width = texture.width,
height = texture.height
} : null,
scale = new { x = scale.x, y = scale.y },
offset = new { x = offset.x, y = offset.y }
};
}
private object HandleSetTexture(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<MaterialSetTextureParams>(request, "gameObject");
var obj = FindGameObject(param.gameObject);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.gameObject}");
}
var renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
throw new Exception($"No Renderer component found on: {param.gameObject}");
}
var material = GetMaterial(renderer, param.materialIndex, param.useShared);
string propName = string.IsNullOrEmpty(param.propertyName) ? "_MainTex" : param.propertyName;
if (!material.HasProperty(propName))
{
throw new Exception($"Material does not have texture property: {propName}");
}
Texture texture = null;
if (!string.IsNullOrEmpty(param.texturePath))
{
texture = AssetDatabase.LoadAssetAtPath<Texture>(param.texturePath);
if (texture == null)
{
throw new Exception($"Texture not found at path: {param.texturePath}");
}
}
material.SetTexture(propName, texture);
// Set scale and offset if provided
if (param.scaleX.HasValue || param.scaleY.HasValue)
{
var currentScale = material.GetTextureScale(propName);
material.SetTextureScale(propName, new Vector2(
param.scaleX ?? currentScale.x,
param.scaleY ?? currentScale.y
));
}
if (param.offsetX.HasValue || param.offsetY.HasValue)
{
var currentOffset = material.GetTextureOffset(propName);
material.SetTextureOffset(propName, new Vector2(
param.offsetX ?? currentOffset.x,
param.offsetY ?? currentOffset.y
));
}
EditorUtility.SetDirty(material);
return new
{
success = true,
gameObject = param.gameObject,
material = material.name,
propertyName = propName,
texture = texture != null ? texture.name : "None"
};
#else
throw new Exception("SetTexture is only available in Unity Editor");
#endif
}
// Helper methods
private Material GetMaterial(Renderer renderer, int? materialIndex, bool useShared)
{
var materials = useShared ? renderer.sharedMaterials : renderer.materials;
int index = materialIndex ?? 0;
if (index < 0 || index >= materials.Length)
{
throw new Exception($"Material index {index} out of range. Available: 0-{materials.Length - 1}");
}
var material = materials[index];
if (material == null)
{
throw new Exception($"Material at index {index} is null");
}
return material;
}
private string GetPropertyType(Material material, string propertyName)
{
#if UNITY_EDITOR
var shader = material.shader;
int propCount = shader.GetPropertyCount();
for (int i = 0; i < propCount; i++)
{
string name = shader.GetPropertyName(i);
if (name == propertyName)
{
var propType = shader.GetPropertyType(i);
return propType.ToString();
}
}
#endif
return "Unknown";
}
// Parameter classes
[Serializable]
public class MaterialBaseParams
{
public string gameObject;
public int? materialIndex;
public bool useShared;
}
[Serializable]
public class MaterialPropertyParams : MaterialBaseParams
{
public string propertyName;
}
[Serializable]
public class MaterialSetPropertyParams : MaterialPropertyParams
{
public float value;
}
[Serializable]
public class MaterialColorParams : MaterialBaseParams
{
public string propertyName;
}
[Serializable]
public class MaterialSetColorParams : MaterialColorParams
{
public float? r;
public float? g;
public float? b;
public float? a;
public string hex;
}
[Serializable]
public class MaterialListParams
{
public string gameObject;
public bool useShared;
}
[Serializable]
public class MaterialSetShaderParams : MaterialBaseParams
{
public string shaderName;
}
[Serializable]
public class MaterialTextureParams : MaterialBaseParams
{
public string propertyName;
}
[Serializable]
public class MaterialSetTextureParams : MaterialTextureParams
{
public string texturePath;
public float? scaleX;
public float? scaleY;
public float? offsetX;
public float? offsetY;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 39c746341355d8b479e91019101c6a4e

View File

@@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEditorToolkit.Protocol;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Unity Editor menu commands
/// </summary>
public class MenuHandler : BaseHandler
{
public override string Category => "Menu";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Run":
return HandleRun(request);
case "List":
return HandleList(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
/// <summary>
/// Execute a menu item by path
/// </summary>
private object HandleRun(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<RunParams>(request, "menuPath");
if (string.IsNullOrWhiteSpace(param.menuPath))
{
throw new Exception("Menu path cannot be empty");
}
try
{
bool success = EditorApplication.ExecuteMenuItem(param.menuPath);
if (!success)
{
throw new Exception($"Menu item not found or execution failed: {param.menuPath}");
}
return new
{
success = true,
menuPath = param.menuPath,
message = $"Menu item executed: {param.menuPath}"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to execute menu item '{param.menuPath}': {ex.Message}");
}
#else
throw new Exception("Menu.Run is only available in Unity Editor");
#endif
}
/// <summary>
/// List available menu items
/// </summary>
private object HandleList(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = request.GetParams<ListParams>() ?? new ListParams();
var menus = new List<MenuItemInfo>();
try
{
// Get menu items using reflection (Unity internal API)
var menuType = typeof(EditorApplication).Assembly.GetType("UnityEditor.Menu");
if (menuType != null)
{
// Try to get menu items using internal methods
var getMenuItemsMethod = menuType.GetMethod("GetMenuItems",
BindingFlags.NonPublic | BindingFlags.Static);
if (getMenuItemsMethod != null)
{
// This method exists but parameters vary by Unity version
// Alternative approach: scan common menu paths
menus = GetKnownMenuItems();
}
else
{
// Fallback: return known menu paths
menus = GetKnownMenuItems();
}
}
else
{
menus = GetKnownMenuItems();
}
// Apply filter if provided
if (!string.IsNullOrEmpty(param.filter))
{
string filterLower = param.filter.ToLower();
bool hasWildcard = param.filter.Contains("*");
if (hasWildcard)
{
// Simple wildcard matching
string pattern = filterLower.Replace("*", "");
menus = menus.Where(m =>
{
string pathLower = m.path.ToLower();
if (param.filter.StartsWith("*") && param.filter.EndsWith("*"))
return pathLower.Contains(pattern);
else if (param.filter.StartsWith("*"))
return pathLower.EndsWith(pattern);
else if (param.filter.EndsWith("*"))
return pathLower.StartsWith(pattern);
return pathLower.Contains(pattern);
}).ToList();
}
else
{
menus = menus.Where(m =>
m.path.ToLower().Contains(filterLower)).ToList();
}
}
return new
{
success = true,
menus = menus.OrderBy(m => m.path).ToList(),
count = menus.Count
};
}
catch (Exception ex)
{
throw new Exception($"Failed to list menu items: {ex.Message}");
}
#else
throw new Exception("Menu.List is only available in Unity Editor");
#endif
}
/// <summary>
/// Get known/common menu items
/// </summary>
private List<MenuItemInfo> GetKnownMenuItems()
{
var menus = new List<MenuItemInfo>();
// File menu
AddMenuCategory(menus, "File", new[]
{
"New Scene", "Open Scene", "Save", "Save As...",
"New Project...", "Open Project...", "Save Project",
"Build Settings...", "Build And Run"
});
// Edit menu
AddMenuCategory(menus, "Edit", new[]
{
"Undo", "Redo", "Cut", "Copy", "Paste", "Duplicate", "Delete",
"Select All", "Deselect All", "Select Children", "Select Prefab Root",
"Play", "Pause", "Step",
"Project Settings...", "Preferences...",
"Clear All PlayerPrefs"
});
// Assets menu
AddMenuCategory(menus, "Assets", new[]
{
"Create/Folder", "Create/C# Script", "Create/Shader/Standard Surface Shader",
"Create/Material", "Create/Prefab", "Create/Scene",
"Open", "Delete", "Rename", "Copy Path",
"Import New Asset...", "Import Package/Custom Package...",
"Export Package...", "Find References In Scene",
"Refresh", "Reimport", "Reimport All"
});
// GameObject menu
AddMenuCategory(menus, "GameObject", new[]
{
"Create Empty", "Create Empty Child",
"3D Object/Cube", "3D Object/Sphere", "3D Object/Capsule",
"3D Object/Cylinder", "3D Object/Plane", "3D Object/Quad",
"2D Object/Sprite", "2D Object/Sprite Mask",
"Effects/Particle System", "Effects/Trail", "Effects/Line",
"Light/Directional Light", "Light/Point Light", "Light/Spotlight",
"Audio/Audio Source", "Audio/Audio Reverb Zone",
"Video/Video Player",
"UI/Canvas", "UI/Panel", "UI/Button", "UI/Text", "UI/Image",
"UI/Raw Image", "UI/Slider", "UI/Scrollbar", "UI/Toggle",
"UI/Input Field", "UI/Dropdown", "UI/Scroll View",
"Camera", "Move To View", "Align With View", "Align View to Selected"
});
// Component menu
AddMenuCategory(menus, "Component", new[]
{
"Add...",
"Mesh/Mesh Filter", "Mesh/Mesh Renderer",
"Physics/Rigidbody", "Physics/Box Collider", "Physics/Sphere Collider",
"Physics/Capsule Collider", "Physics/Mesh Collider",
"Physics 2D/Rigidbody 2D", "Physics 2D/Box Collider 2D",
"Rendering/Camera", "Rendering/Light",
"Audio/Audio Source", "Audio/Audio Listener",
"Scripts"
});
// Window menu
AddMenuCategory(menus, "Window", new[]
{
"General/Scene", "General/Game", "General/Inspector", "General/Hierarchy",
"General/Project", "General/Console",
"Animation/Animation", "Animation/Animator",
"Rendering/Lighting", "Rendering/Light Explorer", "Rendering/Occlusion Culling",
"Audio/Audio Mixer",
"Package Manager",
"2D/Tile Palette", "2D/Sprite Editor",
"AI/Navigation",
"Asset Store",
"Layouts/Default", "Layouts/2 by 3", "Layouts/4 Split", "Layouts/Tall", "Layouts/Wide"
});
// Help menu
AddMenuCategory(menus, "Help", new[]
{
"About Unity...", "Unity Manual", "Scripting Reference",
"Unity Forum", "Unity Answers", "Check for Updates"
});
// Tools menu (common items)
AddMenuCategory(menus, "Tools", new[]
{
"Sprite Editor"
});
return menus;
}
private void AddMenuCategory(List<MenuItemInfo> menus, string category, string[] items)
{
foreach (var item in items)
{
string path = item.Contains("/") ? $"{category}/{item}" : $"{category}/{item}";
menus.Add(new MenuItemInfo { path = path, category = category });
}
}
// Parameter classes
[Serializable]
public class RunParams
{
public string menuPath;
}
[Serializable]
public class ListParams
{
public string filter;
}
[Serializable]
public class MenuItemInfo
{
public string path;
public string category;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 46020fd33042fb949bb1096cc5ad6ec2

View File

@@ -0,0 +1,859 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditorToolkit.Protocol;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Prefab commands (instantiate, create, unpack, apply, revert, variant, etc.)
/// </summary>
public class PrefabHandler : BaseHandler
{
public override string Category => "Prefab";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Instantiate":
return HandleInstantiate(request);
case "Create":
return HandleCreate(request);
case "Unpack":
return HandleUnpack(request);
case "Apply":
return HandleApply(request);
case "Revert":
return HandleRevert(request);
case "Variant":
return HandleVariant(request);
case "GetOverrides":
return HandleGetOverrides(request);
case "GetSource":
return HandleGetSource(request);
case "IsInstance":
return HandleIsInstance(request);
case "Open":
return HandleOpen(request);
case "Close":
return HandleClose(request);
case "List":
return HandleList(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
/// <summary>
/// Instantiate a prefab in the scene
/// </summary>
private object HandleInstantiate(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<InstantiateParams>(request, "path");
// Load prefab asset
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(param.path);
if (prefab == null)
{
throw new Exception($"Prefab not found: {param.path}");
}
// Instantiate prefab
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
if (instance == null)
{
throw new Exception($"Failed to instantiate prefab: {param.path}");
}
// Register undo
Undo.RegisterCreatedObjectUndo(instance, "Instantiate Prefab");
// Set name if provided
if (!string.IsNullOrEmpty(param.name))
{
instance.name = param.name;
}
// Set position if provided
if (!string.IsNullOrEmpty(param.position))
{
var parts = param.position.Split(',');
if (parts.Length == 3)
{
instance.transform.position = new Vector3(
float.Parse(parts[0].Trim()),
float.Parse(parts[1].Trim()),
float.Parse(parts[2].Trim())
);
}
}
// Set rotation if provided
if (!string.IsNullOrEmpty(param.rotation))
{
var parts = param.rotation.Split(',');
if (parts.Length == 3)
{
instance.transform.eulerAngles = new Vector3(
float.Parse(parts[0].Trim()),
float.Parse(parts[1].Trim()),
float.Parse(parts[2].Trim())
);
}
}
// Set parent if provided
if (!string.IsNullOrEmpty(param.parent))
{
var parentObj = FindGameObject(param.parent);
if (parentObj != null)
{
instance.transform.SetParent(parentObj.transform, true);
}
}
return new InstantiateResult
{
success = true,
instanceName = instance.name,
prefabPath = param.path,
position = new Vector3Info(instance.transform.position)
};
#else
throw new Exception("Prefab.Instantiate is only available in Editor mode");
#endif
}
/// <summary>
/// Create a prefab from a scene GameObject
/// </summary>
private object HandleCreate(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<CreateParams>(request, "name and path");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
// Ensure path ends with .prefab
string savePath = param.path;
if (!savePath.EndsWith(".prefab"))
{
savePath += ".prefab";
}
// Create directory if needed
string directory = System.IO.Path.GetDirectoryName(savePath);
if (!string.IsNullOrEmpty(directory) && !AssetDatabase.IsValidFolder(directory))
{
CreateFolderRecursively(directory);
}
// Check if prefab already exists
var existingPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(savePath);
if (existingPrefab != null && !param.overwrite)
{
throw new Exception($"Prefab already exists: {savePath}. Use --overwrite to replace.");
}
// Create prefab
bool success;
GameObject prefab;
if (existingPrefab != null)
{
prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(obj, savePath, InteractionMode.UserAction, out success);
}
else
{
prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(obj, savePath, InteractionMode.UserAction, out success);
}
if (!success || prefab == null)
{
throw new Exception($"Failed to create prefab at: {savePath}");
}
AssetDatabase.Refresh();
return new CreateResult
{
success = true,
prefabPath = savePath,
sourceName = obj.name,
isConnected = PrefabUtility.IsPartOfPrefabInstance(obj)
};
#else
throw new Exception("Prefab.Create is only available in Editor mode");
#endif
}
/// <summary>
/// Unpack a prefab instance
/// </summary>
private object HandleUnpack(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<UnpackParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
if (!PrefabUtility.IsPartOfPrefabInstance(obj))
{
throw new Exception($"GameObject is not a prefab instance: {param.name}");
}
// Get prefab root
var prefabRoot = PrefabUtility.GetOutermostPrefabInstanceRoot(obj);
if (prefabRoot == null)
{
prefabRoot = obj;
}
Undo.RegisterCompleteObjectUndo(prefabRoot, "Unpack Prefab");
// Unpack based on mode
if (param.completely)
{
PrefabUtility.UnpackPrefabInstance(prefabRoot, PrefabUnpackMode.Completely, InteractionMode.UserAction);
}
else
{
PrefabUtility.UnpackPrefabInstance(prefabRoot, PrefabUnpackMode.OutermostRoot, InteractionMode.UserAction);
}
return new { success = true, unpackedObject = prefabRoot.name, completely = param.completely };
#else
throw new Exception("Prefab.Unpack is only available in Editor mode");
#endif
}
/// <summary>
/// Apply prefab instance overrides to the source prefab
/// </summary>
private object HandleApply(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<ApplyParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
if (!PrefabUtility.IsPartOfPrefabInstance(obj))
{
throw new Exception($"GameObject is not a prefab instance: {param.name}");
}
// Get prefab root
var prefabRoot = PrefabUtility.GetOutermostPrefabInstanceRoot(obj);
if (prefabRoot == null)
{
prefabRoot = obj;
}
// Get source prefab path
string prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(prefabRoot);
// Apply all overrides
PrefabUtility.ApplyPrefabInstance(prefabRoot, InteractionMode.UserAction);
return new ApplyResult
{
success = true,
instanceName = prefabRoot.name,
prefabPath = prefabPath
};
#else
throw new Exception("Prefab.Apply is only available in Editor mode");
#endif
}
/// <summary>
/// Revert prefab instance overrides
/// </summary>
private object HandleRevert(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<RevertParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
if (!PrefabUtility.IsPartOfPrefabInstance(obj))
{
throw new Exception($"GameObject is not a prefab instance: {param.name}");
}
// Get prefab root
var prefabRoot = PrefabUtility.GetOutermostPrefabInstanceRoot(obj);
if (prefabRoot == null)
{
prefabRoot = obj;
}
Undo.RegisterCompleteObjectUndo(prefabRoot, "Revert Prefab");
// Revert all overrides
PrefabUtility.RevertPrefabInstance(prefabRoot, InteractionMode.UserAction);
return new { success = true, revertedObject = prefabRoot.name };
#else
throw new Exception("Prefab.Revert is only available in Editor mode");
#endif
}
/// <summary>
/// Create a prefab variant
/// </summary>
private object HandleVariant(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<VariantParams>(request, "sourcePath and variantPath");
// Load source prefab
var sourcePrefab = AssetDatabase.LoadAssetAtPath<GameObject>(param.sourcePath);
if (sourcePrefab == null)
{
throw new Exception($"Source prefab not found: {param.sourcePath}");
}
// Ensure path ends with .prefab
string variantPath = param.variantPath;
if (!variantPath.EndsWith(".prefab"))
{
variantPath += ".prefab";
}
// Create directory if needed
string directory = System.IO.Path.GetDirectoryName(variantPath);
if (!string.IsNullOrEmpty(directory) && !AssetDatabase.IsValidFolder(directory))
{
CreateFolderRecursively(directory);
}
// Instantiate temporarily to create variant
var tempInstance = (GameObject)PrefabUtility.InstantiatePrefab(sourcePrefab);
// Create variant
var variant = PrefabUtility.SaveAsPrefabAsset(tempInstance, variantPath);
// Cleanup temp instance
UnityEngine.Object.DestroyImmediate(tempInstance);
if (variant == null)
{
throw new Exception($"Failed to create variant at: {variantPath}");
}
AssetDatabase.Refresh();
return new VariantResult
{
success = true,
sourcePath = param.sourcePath,
variantPath = variantPath
};
#else
throw new Exception("Prefab.Variant is only available in Editor mode");
#endif
}
/// <summary>
/// Get prefab instance overrides
/// </summary>
private object HandleGetOverrides(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<GetOverridesParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
if (!PrefabUtility.IsPartOfPrefabInstance(obj))
{
throw new Exception($"GameObject is not a prefab instance: {param.name}");
}
// Get prefab root
var prefabRoot = PrefabUtility.GetOutermostPrefabInstanceRoot(obj);
if (prefabRoot == null)
{
prefabRoot = obj;
}
// Get all overrides
var objectOverrides = PrefabUtility.GetObjectOverrides(prefabRoot);
var addedComponents = PrefabUtility.GetAddedComponents(prefabRoot);
var removedComponents = PrefabUtility.GetRemovedComponents(prefabRoot);
var addedGameObjects = PrefabUtility.GetAddedGameObjects(prefabRoot);
var overrideList = new List<OverrideInfo>();
foreach (var ov in objectOverrides)
{
overrideList.Add(new OverrideInfo
{
type = "PropertyOverride",
targetName = ov.instanceObject?.name ?? "Unknown",
targetType = ov.instanceObject?.GetType().Name ?? "Unknown"
});
}
foreach (var ac in addedComponents)
{
overrideList.Add(new OverrideInfo
{
type = "AddedComponent",
targetName = ac.instanceComponent?.name ?? "Unknown",
targetType = ac.instanceComponent?.GetType().Name ?? "Unknown"
});
}
foreach (var rc in removedComponents)
{
overrideList.Add(new OverrideInfo
{
type = "RemovedComponent",
targetName = rc.assetComponent?.name ?? "Unknown",
targetType = rc.assetComponent?.GetType().Name ?? "Unknown"
});
}
foreach (var ag in addedGameObjects)
{
overrideList.Add(new OverrideInfo
{
type = "AddedGameObject",
targetName = ag.instanceGameObject?.name ?? "Unknown",
targetType = "GameObject"
});
}
return new GetOverridesResult
{
instanceName = prefabRoot.name,
hasOverrides = overrideList.Count > 0,
overrideCount = overrideList.Count,
overrides = overrideList
};
#else
throw new Exception("Prefab.GetOverrides is only available in Editor mode");
#endif
}
/// <summary>
/// Get source prefab path of an instance
/// </summary>
private object HandleGetSource(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<GetSourceParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
if (!PrefabUtility.IsPartOfPrefabInstance(obj))
{
return new GetSourceResult
{
instanceName = obj.name,
isPrefabInstance = false,
prefabPath = null,
prefabType = "None"
};
}
string prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(obj);
var prefabType = PrefabUtility.GetPrefabAssetType(obj);
var prefabStatus = PrefabUtility.GetPrefabInstanceStatus(obj);
return new GetSourceResult
{
instanceName = obj.name,
isPrefabInstance = true,
prefabPath = prefabPath,
prefabType = prefabType.ToString(),
prefabStatus = prefabStatus.ToString()
};
#else
throw new Exception("Prefab.GetSource is only available in Editor mode");
#endif
}
/// <summary>
/// Check if a GameObject is a prefab instance
/// </summary>
private object HandleIsInstance(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<IsInstanceParams>(request, "name");
var obj = FindGameObject(param.name);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.name}");
}
bool isPrefabInstance = PrefabUtility.IsPartOfPrefabInstance(obj);
bool isPrefabAsset = PrefabUtility.IsPartOfPrefabAsset(obj);
bool isOutermostRoot = PrefabUtility.IsOutermostPrefabInstanceRoot(obj);
var prefabType = PrefabUtility.GetPrefabAssetType(obj);
return new IsInstanceResult
{
name = obj.name,
isPrefabInstance = isPrefabInstance,
isPrefabAsset = isPrefabAsset,
isOutermostRoot = isOutermostRoot,
prefabType = prefabType.ToString()
};
#else
throw new Exception("Prefab.IsInstance is only available in Editor mode");
#endif
}
/// <summary>
/// Open a prefab in prefab editing mode
/// </summary>
private object HandleOpen(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<OpenParams>(request, "path");
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(param.path);
if (prefab == null)
{
throw new Exception($"Prefab not found: {param.path}");
}
// Open prefab stage
AssetDatabase.OpenAsset(prefab);
var stage = PrefabStageUtility.GetCurrentPrefabStage();
if (stage == null)
{
throw new Exception($"Failed to open prefab: {param.path}");
}
return new OpenResult
{
success = true,
prefabPath = param.path,
prefabName = prefab.name,
stageRoot = stage.prefabContentsRoot?.name
};
#else
throw new Exception("Prefab.Open is only available in Editor mode");
#endif
}
/// <summary>
/// Close prefab editing mode and return to scene
/// </summary>
private object HandleClose(JsonRpcRequest request)
{
#if UNITY_EDITOR
var stage = PrefabStageUtility.GetCurrentPrefabStage();
if (stage == null)
{
return new { success = true, message = "No prefab stage is currently open" };
}
string prefabPath = stage.assetPath;
// Close prefab stage by returning to main stage
StageUtility.GoToMainStage();
return new { success = true, closedPrefab = prefabPath };
#else
throw new Exception("Prefab.Close is only available in Editor mode");
#endif
}
/// <summary>
/// List all prefabs in a folder
/// </summary>
private object HandleList(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<ListParams>(request, "");
string searchPath = string.IsNullOrEmpty(param.path) ? "Assets" : param.path;
// Find all prefab GUIDs
string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { searchPath });
var prefabs = new List<PrefabInfo>();
foreach (var guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
if (prefab != null)
{
var prefabType = PrefabUtility.GetPrefabAssetType(prefab);
prefabs.Add(new PrefabInfo
{
name = prefab.name,
path = assetPath,
type = prefabType.ToString(),
isVariant = prefabType == PrefabAssetType.Variant
});
}
}
return new ListResult
{
count = prefabs.Count,
searchPath = searchPath,
prefabs = prefabs
};
#else
throw new Exception("Prefab.List is only available in Editor mode");
#endif
}
#region Helper Methods
private void CreateFolderRecursively(string path)
{
#if UNITY_EDITOR
if (AssetDatabase.IsValidFolder(path))
return;
string parent = System.IO.Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parent) && !AssetDatabase.IsValidFolder(parent))
{
CreateFolderRecursively(parent);
}
string folderName = System.IO.Path.GetFileName(path);
AssetDatabase.CreateFolder(parent, folderName);
#endif
}
#endregion
#region Parameter Classes
[Serializable]
private class InstantiateParams
{
public string path;
public string name;
public string position;
public string rotation;
public string parent;
}
[Serializable]
private class CreateParams
{
public string name;
public string path;
public bool overwrite;
}
[Serializable]
private class UnpackParams
{
public string name;
public bool completely;
}
[Serializable]
private class ApplyParams
{
public string name;
}
[Serializable]
private class RevertParams
{
public string name;
}
[Serializable]
private class VariantParams
{
public string sourcePath;
public string variantPath;
}
[Serializable]
private class GetOverridesParams
{
public string name;
}
[Serializable]
private class GetSourceParams
{
public string name;
}
[Serializable]
private class IsInstanceParams
{
public string name;
}
[Serializable]
private class OpenParams
{
public string path;
}
[Serializable]
private class ListParams
{
public string path;
}
#endregion
#region Response Classes
[Serializable]
public class Vector3Info
{
public float x;
public float y;
public float z;
public Vector3Info(Vector3 v)
{
x = v.x;
y = v.y;
z = v.z;
}
}
[Serializable]
public class InstantiateResult
{
public bool success;
public string instanceName;
public string prefabPath;
public Vector3Info position;
}
[Serializable]
public class CreateResult
{
public bool success;
public string prefabPath;
public string sourceName;
public bool isConnected;
}
[Serializable]
public class ApplyResult
{
public bool success;
public string instanceName;
public string prefabPath;
}
[Serializable]
public class VariantResult
{
public bool success;
public string sourcePath;
public string variantPath;
}
[Serializable]
public class OverrideInfo
{
public string type;
public string targetName;
public string targetType;
}
[Serializable]
public class GetOverridesResult
{
public string instanceName;
public bool hasOverrides;
public int overrideCount;
public List<OverrideInfo> overrides;
}
[Serializable]
public class GetSourceResult
{
public string instanceName;
public bool isPrefabInstance;
public string prefabPath;
public string prefabType;
public string prefabStatus;
}
[Serializable]
public class IsInstanceResult
{
public string name;
public bool isPrefabInstance;
public bool isPrefabAsset;
public bool isOutermostRoot;
public string prefabType;
}
[Serializable]
public class OpenResult
{
public bool success;
public string prefabPath;
public string prefabName;
public string stageRoot;
}
[Serializable]
public class PrefabInfo
{
public string name;
public string path;
public string type;
public bool isVariant;
}
[Serializable]
public class ListResult
{
public int count;
public string searchPath;
public List<PrefabInfo> prefabs;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 37b6a770044b2b248a23949937838244

View File

@@ -0,0 +1,443 @@
using System;
using UnityEngine;
using UnityEditorToolkit.Protocol;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for EditorPrefs commands
/// </summary>
public class PrefsHandler : BaseHandler
{
public override string Category => "Prefs";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "GetString":
return HandleGetString(request);
case "GetInt":
return HandleGetInt(request);
case "GetFloat":
return HandleGetFloat(request);
case "GetBool":
return HandleGetBool(request);
case "SetString":
return HandleSetString(request);
case "SetInt":
return HandleSetInt(request);
case "SetFloat":
return HandleSetFloat(request);
case "SetBool":
return HandleSetBool(request);
case "DeleteKey":
return HandleDeleteKey(request);
case "DeleteAll":
return HandleDeleteAll(request);
case "HasKey":
return HandleHasKey(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
private object HandleGetString(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<GetPrefsParams>(request, "key");
try
{
string value = EditorPrefs.GetString(param.key, param.defaultValue ?? "");
return new
{
success = true,
key = param.key,
value = value,
type = "string"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to get EditorPrefs string: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleGetInt(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<GetPrefsParams>(request, "key");
try
{
int defaultValue = 0;
if (param.defaultValue != null)
{
int.TryParse(param.defaultValue, out defaultValue);
}
int value = EditorPrefs.GetInt(param.key, defaultValue);
return new
{
success = true,
key = param.key,
value = value,
type = "int"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to get EditorPrefs int: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleGetFloat(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<GetPrefsParams>(request, "key");
try
{
float defaultValue = 0f;
if (param.defaultValue != null)
{
float.TryParse(param.defaultValue, out defaultValue);
}
float value = EditorPrefs.GetFloat(param.key, defaultValue);
return new
{
success = true,
key = param.key,
value = value,
type = "float"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to get EditorPrefs float: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleGetBool(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<GetPrefsParams>(request, "key");
try
{
bool defaultValue = false;
if (param.defaultValue != null)
{
bool.TryParse(param.defaultValue, out defaultValue);
}
bool value = EditorPrefs.GetBool(param.key, defaultValue);
return new
{
success = true,
key = param.key,
value = value,
type = "bool"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to get EditorPrefs bool: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleSetString(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<SetPrefsParams>(request, "params");
try
{
EditorPrefs.SetString(param.key, param.value);
return new
{
success = true,
key = param.key,
message = "EditorPrefs string value set"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to set EditorPrefs string: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleSetInt(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<SetPrefsParams>(request, "params");
try
{
if (!int.TryParse(param.value, out int intValue))
{
throw new Exception($"Invalid int value: {param.value}");
}
EditorPrefs.SetInt(param.key, intValue);
return new
{
success = true,
key = param.key,
message = "EditorPrefs int value set"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to set EditorPrefs int: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleSetFloat(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<SetPrefsParams>(request, "params");
try
{
if (!float.TryParse(param.value, out float floatValue))
{
throw new Exception($"Invalid float value: {param.value}");
}
EditorPrefs.SetFloat(param.key, floatValue);
return new
{
success = true,
key = param.key,
message = "EditorPrefs float value set"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to set EditorPrefs float: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleSetBool(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<SetPrefsParams>(request, "params");
try
{
if (!bool.TryParse(param.value, out bool boolValue))
{
throw new Exception($"Invalid bool value: {param.value}");
}
EditorPrefs.SetBool(param.key, boolValue);
return new
{
success = true,
key = param.key,
message = "EditorPrefs bool value set"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to set EditorPrefs bool: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleDeleteKey(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<DeleteKeyParams>(request, "key");
try
{
EditorPrefs.DeleteKey(param.key);
return new
{
success = true,
key = param.key,
message = "EditorPrefs key deleted"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to delete EditorPrefs key: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleDeleteAll(JsonRpcRequest request)
{
#if UNITY_EDITOR
try
{
EditorPrefs.DeleteAll();
return new
{
success = true,
message = "All EditorPrefs deleted"
};
}
catch (Exception ex)
{
throw new Exception($"Failed to delete all EditorPrefs: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
private object HandleHasKey(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<HasKeyParams>(request, "key");
try
{
bool hasKey = EditorPrefs.HasKey(param.key);
// 키가 존재하면 값과 타입도 함께 반환
if (hasKey)
{
// 타입 감지: int, float, bool, string 순서로 시도
object value = null;
string valueType = "unknown";
// Int 시도
try
{
int intValue = EditorPrefs.GetInt(param.key, int.MinValue);
string strValue = EditorPrefs.GetString(param.key, "");
// Int로 저장된 값이면 문자열 버전이 없거나 숫자 형태
if (string.IsNullOrEmpty(strValue) || int.TryParse(strValue, out _))
{
value = intValue;
valueType = "int";
}
}
catch { }
// Bool 시도 (int로 저장되므로 0 또는 1인지 확인)
if (valueType == "int" && (int)value >= 0 && (int)value <= 1)
{
bool boolValue = EditorPrefs.GetBool(param.key, false);
value = boolValue;
valueType = "bool";
}
// Float 시도
if (valueType == "unknown")
{
try
{
float floatValue = EditorPrefs.GetFloat(param.key, float.MinValue);
if (floatValue != float.MinValue)
{
value = floatValue;
valueType = "float";
}
}
catch { }
}
// String (기본값)
if (valueType == "unknown" || valueType == "int")
{
string strValue = EditorPrefs.GetString(param.key, "");
if (!string.IsNullOrEmpty(strValue))
{
value = strValue;
valueType = "string";
}
}
return new
{
success = true,
key = param.key,
hasKey = true,
type = valueType,
value = value
};
}
else
{
return new
{
success = true,
key = param.key,
hasKey = false
};
}
}
catch (Exception ex)
{
throw new Exception($"Failed to check EditorPrefs key: {ex.Message}");
}
#else
throw new Exception("EditorPrefs is only available in Unity Editor");
#endif
}
// Parameter classes
[Serializable]
public class GetPrefsParams
{
public string key;
public string defaultValue;
}
[Serializable]
public class SetPrefsParams
{
public string key;
public string value;
}
[Serializable]
public class DeleteKeyParams
{
public string key;
}
[Serializable]
public class HasKeyParams
{
public string key;
}
}
}

View File

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

View File

@@ -0,0 +1,312 @@
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);
case "New":
return HandleNew(request);
case "Save":
return HandleSave(request);
case "Unload":
return HandleUnload(request);
case "SetActive":
return HandleSetActive(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 object HandleNew(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = request.GetParams<NewSceneParams>() ?? new NewSceneParams();
try
{
var setup = param.empty ? NewSceneSetup.EmptyScene : NewSceneSetup.DefaultGameObjects;
var mode = param.additive ? NewSceneMode.Additive : NewSceneMode.Single;
var scene = EditorSceneManager.NewScene(setup, mode);
return new
{
success = true,
scene = GetSceneInfo(scene)
};
}
catch (Exception ex)
{
throw new Exception($"Failed to create new scene: {ex.Message}");
}
#else
throw new Exception("New scene is only available in Editor mode");
#endif
}
private object HandleSave(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = request.GetParams<SaveSceneParams>() ?? new SaveSceneParams();
try
{
Scene scene;
// 특정 씬 이름이 지정된 경우 해당 씬 찾기
if (!string.IsNullOrEmpty(param.sceneName))
{
scene = SceneManager.GetSceneByName(param.sceneName);
if (!scene.IsValid())
{
scene = SceneManager.GetSceneByPath(param.sceneName);
}
if (!scene.IsValid())
{
throw new Exception($"Scene not found: {param.sceneName}");
}
}
else
{
scene = SceneManager.GetActiveScene();
}
bool saved;
// 새 경로로 저장 (Save As)
if (!string.IsNullOrEmpty(param.path))
{
saved = EditorSceneManager.SaveScene(scene, param.path);
}
else
{
saved = EditorSceneManager.SaveScene(scene);
}
return new
{
success = saved,
scene = GetSceneInfo(scene)
};
}
catch (Exception ex)
{
throw new Exception($"Failed to save scene: {ex.Message}");
}
#else
throw new Exception("Save scene is only available in Editor mode");
#endif
}
private object HandleUnload(JsonRpcRequest request)
{
var param = ValidateParam<UnloadSceneParams>(request, "name");
#if UNITY_EDITOR
try
{
Scene scene = SceneManager.GetSceneByName(param.name);
if (!scene.IsValid())
{
scene = SceneManager.GetSceneByPath(param.name);
}
if (!scene.IsValid())
{
throw new Exception($"Scene not found: {param.name}");
}
// 마지막 씬은 언로드 불가
if (SceneManager.sceneCount <= 1)
{
throw new Exception("Cannot unload the last scene");
}
bool closed = EditorSceneManager.CloseScene(scene, param.removeScene);
return new { success = closed };
}
catch (Exception ex)
{
throw new Exception($"Failed to unload scene: {ex.Message}");
}
#else
try
{
SceneManager.UnloadSceneAsync(param.name);
return new { success = true };
}
catch (Exception ex)
{
throw new Exception($"Failed to unload scene: {ex.Message}");
}
#endif
}
private object HandleSetActive(JsonRpcRequest request)
{
var param = ValidateParam<SetActiveParams>(request, "name");
try
{
Scene scene = SceneManager.GetSceneByName(param.name);
if (!scene.IsValid())
{
scene = SceneManager.GetSceneByPath(param.name);
}
if (!scene.IsValid())
{
throw new Exception($"Scene not found: {param.name}");
}
if (!scene.isLoaded)
{
throw new Exception($"Scene is not loaded: {param.name}");
}
bool success = SceneManager.SetActiveScene(scene);
return new
{
success = success,
scene = GetSceneInfo(scene)
};
}
catch (Exception ex)
{
throw new Exception($"Failed to set active scene: {ex.Message}");
}
}
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 NewSceneParams
{
public bool empty; // true: 빈 씬, false: 기본 오브젝트 포함
public bool additive; // true: 추가 모드, false: 기존 씬 대체
}
[Serializable]
public class SaveSceneParams
{
public string sceneName; // 저장할 씬 이름 (없으면 활성 씬)
public string path; // 저장 경로 (없으면 현재 경로에 저장)
}
[Serializable]
public class UnloadSceneParams
{
public string name;
public bool removeScene; // true: 씬 완전 제거, false: 언로드만
}
[Serializable]
public class SetActiveParams
{
public string name;
}
[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: 2d8087eb6a24b1046a03e8c326cc8245

View File

@@ -0,0 +1,645 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Database;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Snapshot commands (Save/Load scene state to/from database)
/// </summary>
public class SnapshotHandler : BaseHandler
{
public override string Category => "Snapshot";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Save":
return HandleSave(request);
case "List":
return HandleList(request);
case "Restore":
return HandleRestore(request);
case "Delete":
return HandleDelete(request);
case "Get":
return HandleGet(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
/// <summary>
/// Save current scene state as a snapshot
/// </summary>
private object HandleSave(JsonRpcRequest request)
{
var param = ValidateParam<SaveParams>(request, "name");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var scene = SceneManager.GetActiveScene();
// Get or create scene record
int sceneId = EnsureSceneRecord(connection, scene);
// Serialize scene state
var snapshotData = SerializeSceneState(scene);
var snapshotJson = JsonUtility.ToJson(snapshotData);
// Insert snapshot
var sql = @"
INSERT INTO snapshots (scene_id, snapshot_name, snapshot_data, description, created_at)
VALUES (?, ?, ?, ?, datetime('now', 'localtime'))
";
connection.Execute(sql, sceneId, param.name, snapshotJson, param.description ?? "");
// Get the inserted snapshot ID
var lastIdSql = "SELECT last_insert_rowid()";
var snapshotId = connection.ExecuteScalar<int>(lastIdSql);
return new SnapshotResult
{
success = true,
snapshotId = snapshotId,
snapshotName = param.name,
sceneName = scene.name,
scenePath = scene.path,
objectCount = snapshotData.objects.Count,
message = $"Snapshot '{param.name}' saved with {snapshotData.objects.Count} objects"
};
}
/// <summary>
/// List all snapshots for current scene or all scenes
/// </summary>
private object HandleList(JsonRpcRequest request)
{
var param = request.GetParams<ListParams>() ?? new ListParams();
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
string sql;
List<SnapshotRecord> records;
if (param.allScenes)
{
sql = @"
SELECT s.snapshot_id, s.scene_id, sc.scene_name, sc.scene_path,
s.snapshot_name, s.description, s.created_at
FROM snapshots s
INNER JOIN scenes sc ON s.scene_id = sc.scene_id
ORDER BY s.created_at DESC
LIMIT ?
";
records = connection.Query<SnapshotRecord>(sql, param.limit);
}
else
{
var scene = SceneManager.GetActiveScene();
sql = @"
SELECT s.snapshot_id, s.scene_id, sc.scene_name, sc.scene_path,
s.snapshot_name, s.description, s.created_at
FROM snapshots s
INNER JOIN scenes sc ON s.scene_id = sc.scene_id
WHERE sc.scene_path = ?
ORDER BY s.created_at DESC
LIMIT ?
";
records = connection.Query<SnapshotRecord>(sql, scene.path, param.limit);
}
var snapshots = records.Select(r => new SnapshotInfo
{
snapshotId = r.snapshot_id,
sceneId = r.scene_id,
sceneName = r.scene_name,
scenePath = r.scene_path,
snapshotName = r.snapshot_name,
description = r.description,
createdAt = r.created_at
}).ToList();
return new ListResult
{
success = true,
count = snapshots.Count,
snapshots = snapshots
};
}
/// <summary>
/// Get snapshot details by ID
/// </summary>
private object HandleGet(JsonRpcRequest request)
{
var param = ValidateParam<GetParams>(request, "snapshotId");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var sql = @"
SELECT s.snapshot_id, s.scene_id, sc.scene_name, sc.scene_path,
s.snapshot_name, s.snapshot_data, s.description, s.created_at
FROM snapshots s
INNER JOIN scenes sc ON s.scene_id = sc.scene_id
WHERE s.snapshot_id = ?
";
var records = connection.Query<SnapshotDataRecord>(sql, param.snapshotId);
if (records.Count() == 0)
{
throw new Exception($"Snapshot with ID {param.snapshotId} not found");
}
var record = records[0];
var snapshotData = JsonUtility.FromJson<SceneSnapshotData>(record.snapshot_data);
return new GetResult
{
success = true,
snapshotId = record.snapshot_id,
sceneId = record.scene_id,
sceneName = record.scene_name,
scenePath = record.scene_path,
snapshotName = record.snapshot_name,
description = record.description,
createdAt = record.created_at,
objectCount = snapshotData.objects.Count,
data = snapshotData
};
}
/// <summary>
/// Restore scene from snapshot
/// </summary>
private object HandleRestore(JsonRpcRequest request)
{
var param = ValidateParam<RestoreParams>(request, "snapshotId");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
// Get snapshot data
var sql = @"
SELECT s.snapshot_id, s.scene_id, sc.scene_name, sc.scene_path,
s.snapshot_name, s.snapshot_data, s.description, s.created_at
FROM snapshots s
INNER JOIN scenes sc ON s.scene_id = sc.scene_id
WHERE s.snapshot_id = ?
";
var records = connection.Query<SnapshotDataRecord>(sql, param.snapshotId);
if (records.Count() == 0)
{
throw new Exception($"Snapshot with ID {param.snapshotId} not found");
}
var record = records[0];
var snapshotData = JsonUtility.FromJson<SceneSnapshotData>(record.snapshot_data);
// Restore scene state
int restoredCount = RestoreSceneState(snapshotData, param.clearScene);
return new RestoreResult
{
success = true,
snapshotId = record.snapshot_id,
snapshotName = record.snapshot_name,
sceneName = record.scene_name,
restoredObjects = restoredCount,
message = $"Restored {restoredCount} objects from snapshot '{record.snapshot_name}'"
};
}
/// <summary>
/// Delete a snapshot
/// </summary>
private object HandleDelete(JsonRpcRequest request)
{
var param = ValidateParam<DeleteParams>(request, "snapshotId");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
// Check if snapshot exists
var checkSql = "SELECT snapshot_name FROM snapshots WHERE snapshot_id = ?";
var names = connection.Query<NameRecord>(checkSql, param.snapshotId);
if (names.Count() == 0)
{
throw new Exception($"Snapshot with ID {param.snapshotId} not found");
}
var snapshotName = names[0].snapshot_name;
// Delete snapshot
var deleteSql = "DELETE FROM snapshots WHERE snapshot_id = ?";
connection.Execute(deleteSql, param.snapshotId);
return new DeleteResult
{
success = true,
snapshotId = param.snapshotId,
snapshotName = snapshotName,
message = $"Snapshot '{snapshotName}' deleted"
};
}
#region Helper Methods
private int EnsureSceneRecord(SQLite.SQLiteConnection connection, Scene scene)
{
var checkSql = "SELECT scene_id FROM scenes WHERE scene_path = ?";
var ids = connection.Query<SceneIdRecord>(checkSql, scene.path);
if (ids.Count() > 0)
{
return ids[0].scene_id;
}
// Insert new scene record
var insertSql = @"
INSERT INTO scenes (scene_name, scene_path, build_index, is_loaded, created_at, updated_at)
VALUES (?, ?, ?, 1, datetime('now', 'localtime'), datetime('now', 'localtime'))
";
connection.Execute(insertSql, scene.name, scene.path, scene.buildIndex);
var lastIdSql = "SELECT last_insert_rowid()";
return connection.ExecuteScalar<int>(lastIdSql);
}
private SceneSnapshotData SerializeSceneState(Scene scene)
{
var data = new SceneSnapshotData
{
sceneName = scene.name,
scenePath = scene.path,
buildIndex = scene.buildIndex,
capturedAt = DateTime.Now.ToString("O"),
objects = new List<GameObjectData>()
};
// Get all root objects
var rootObjects = scene.GetRootGameObjects();
foreach (var root in rootObjects)
{
SerializeGameObjectRecursive(root, data.objects);
}
return data;
}
private void SerializeGameObjectRecursive(GameObject obj, List<GameObjectData> list)
{
var objData = new GameObjectData
{
instanceId = obj.GetInstanceID(),
name = obj.name,
tag = obj.tag,
layer = obj.layer,
isActive = obj.activeSelf,
isStatic = obj.isStatic,
transform = new TransformData
{
positionX = obj.transform.localPosition.x,
positionY = obj.transform.localPosition.y,
positionZ = obj.transform.localPosition.z,
rotationX = obj.transform.localRotation.x,
rotationY = obj.transform.localRotation.y,
rotationZ = obj.transform.localRotation.z,
rotationW = obj.transform.localRotation.w,
scaleX = obj.transform.localScale.x,
scaleY = obj.transform.localScale.y,
scaleZ = obj.transform.localScale.z
},
children = new List<GameObjectData>()
};
list.Add(objData);
// Serialize children
for (int i = 0; i < obj.transform.childCount; i++)
{
var child = obj.transform.GetChild(i).gameObject;
SerializeGameObjectRecursive(child, objData.children);
}
}
private int RestoreSceneState(SceneSnapshotData data, bool clearScene)
{
#if UNITY_EDITOR
if (clearScene)
{
// Clear current scene objects (except preserved ones)
var currentRoots = SceneManager.GetActiveScene().GetRootGameObjects();
foreach (var obj in currentRoots)
{
if (!obj.name.StartsWith("_") && obj.tag != "EditorOnly")
{
Undo.DestroyObjectImmediate(obj);
}
}
}
#endif
int restoredCount = 0;
// Restore objects
foreach (var objData in data.objects)
{
restoredCount += RestoreGameObjectRecursive(objData, null);
}
return restoredCount;
}
private int RestoreGameObjectRecursive(GameObjectData data, Transform parent)
{
int count = 0;
// Try to find existing object by instance ID or name
GameObject obj = null;
#if UNITY_EDITOR
obj = EditorUtility.InstanceIDToObject(data.instanceId) as GameObject;
#endif
if (obj == null)
{
// Create new object
obj = new GameObject(data.name);
#if UNITY_EDITOR
Undo.RegisterCreatedObjectUndo(obj, "Restore Snapshot");
#endif
}
else
{
#if UNITY_EDITOR
Undo.RecordObject(obj.transform, "Restore Snapshot");
Undo.RecordObject(obj, "Restore Snapshot");
#endif
}
// Restore properties
obj.name = data.name;
obj.tag = data.tag;
obj.layer = data.layer;
obj.SetActive(data.isActive);
obj.isStatic = data.isStatic;
if (parent != null)
{
obj.transform.SetParent(parent, false);
}
// Restore transform
obj.transform.localPosition = new Vector3(data.transform.positionX, data.transform.positionY, data.transform.positionZ);
obj.transform.localRotation = new Quaternion(data.transform.rotationX, data.transform.rotationY, data.transform.rotationZ, data.transform.rotationW);
obj.transform.localScale = new Vector3(data.transform.scaleX, data.transform.scaleY, data.transform.scaleZ);
count++;
// Restore children
foreach (var childData in data.children)
{
count += RestoreGameObjectRecursive(childData, obj.transform);
}
return count;
}
#endregion
#region Data Classes
// Parameter classes
[Serializable]
public class SaveParams
{
public string name;
public string description;
}
[Serializable]
public class ListParams
{
public bool allScenes = false;
public int limit = 50;
}
[Serializable]
public class GetParams
{
public int snapshotId;
}
[Serializable]
public class RestoreParams
{
public int snapshotId;
public bool clearScene = false;
}
[Serializable]
public class DeleteParams
{
public int snapshotId;
}
// Database record classes
private class SnapshotRecord
{
public int snapshot_id { get; set; }
public int scene_id { get; set; }
public string scene_name { get; set; }
public string scene_path { get; set; }
public string snapshot_name { get; set; }
public string description { get; set; }
public string created_at { get; set; }
}
private class SnapshotDataRecord
{
public int snapshot_id { get; set; }
public int scene_id { get; set; }
public string scene_name { get; set; }
public string scene_path { get; set; }
public string snapshot_name { get; set; }
public string snapshot_data { get; set; }
public string description { get; set; }
public string created_at { get; set; }
}
private class SceneIdRecord
{
public int scene_id { get; set; }
}
private class NameRecord
{
public string snapshot_name { get; set; }
}
// Result classes
[Serializable]
public class SnapshotResult
{
public bool success;
public int snapshotId;
public string snapshotName;
public string sceneName;
public string scenePath;
public int objectCount;
public string message;
}
[Serializable]
public class ListResult
{
public bool success;
public int count;
public List<SnapshotInfo> snapshots;
}
[Serializable]
public class SnapshotInfo
{
public int snapshotId;
public int sceneId;
public string sceneName;
public string scenePath;
public string snapshotName;
public string description;
public string createdAt;
}
[Serializable]
public class GetResult
{
public bool success;
public int snapshotId;
public int sceneId;
public string sceneName;
public string scenePath;
public string snapshotName;
public string description;
public string createdAt;
public int objectCount;
public SceneSnapshotData data;
}
[Serializable]
public class RestoreResult
{
public bool success;
public int snapshotId;
public string snapshotName;
public string sceneName;
public int restoredObjects;
public string message;
}
[Serializable]
public class DeleteResult
{
public bool success;
public int snapshotId;
public string snapshotName;
public string message;
}
// Snapshot data classes
[Serializable]
public class SceneSnapshotData
{
public string sceneName;
public string scenePath;
public int buildIndex;
public string capturedAt;
public List<GameObjectData> objects;
}
[Serializable]
public class GameObjectData
{
public int instanceId;
public string name;
public string tag;
public int layer;
public bool isActive;
public bool isStatic;
public TransformData transform;
public List<GameObjectData> children;
}
[Serializable]
public class TransformData
{
public float positionX;
public float positionY;
public float positionZ;
public float rotationX;
public float rotationY;
public float rotationZ;
public float rotationW;
public float scaleX;
public float scaleY;
public float scaleZ;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,737 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Database;
using UnityEditorToolkit.Editor.Utils;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for syncing Unity GameObjects and Components with database
/// </summary>
public class SyncHandler : BaseHandler
{
public override string Category => "Sync";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "SyncScene":
return HandleSyncScene(request);
case "SyncGameObject":
return HandleSyncGameObject(request);
case "GetSyncStatus":
return HandleGetSyncStatus(request);
case "ClearSync":
return HandleClearSync(request);
case "StartAutoSync":
return HandleStartAutoSync(request);
case "StopAutoSync":
return HandleStopAutoSync(request);
case "GetAutoSyncStatus":
return HandleGetAutoSyncStatus(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
/// <summary>
/// Sync entire scene to database
/// </summary>
private object HandleSyncScene(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var param = request.GetParams<SyncSceneParams>() ?? new SyncSceneParams();
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var scene = SceneManager.GetActiveScene();
// Ensure scene record exists
int sceneId = EnsureSceneRecord(connection, scene);
int syncedObjects = 0;
int syncedComponents = 0;
int closureRecords = 0;
// Get all GameObjects in scene
var allObjects = scene.GetRootGameObjects();
var objectList = new List<GameObject>();
// Collect all objects recursively
foreach (var root in allObjects)
{
CollectGameObjectsRecursive(root, objectList);
}
// Begin transaction for performance
connection.BeginTransaction();
try
{
// Clear existing data if requested
if (param.clearExisting)
{
connection.Execute("DELETE FROM gameobject_closure WHERE descendant_id IN (SELECT object_id FROM gameobjects WHERE scene_id = ?)", sceneId);
connection.Execute("DELETE FROM components WHERE object_id IN (SELECT object_id FROM gameobjects WHERE scene_id = ?)", sceneId);
connection.Execute("DELETE FROM gameobjects WHERE scene_id = ?", sceneId);
}
// Sync GameObjects
var objectIdMap = new Dictionary<int, int>(); // instanceId -> object_id
foreach (var obj in objectList)
{
int objectId = SyncGameObject(connection, obj, sceneId, objectIdMap);
objectIdMap[obj.GetInstanceID()] = objectId;
syncedObjects++;
// Sync components if requested
if (param.includeComponents)
{
syncedComponents += SyncComponents(connection, obj, objectId);
}
}
// Build closure table
if (param.buildClosure)
{
closureRecords = BuildClosureTable(connection, objectList, objectIdMap);
}
connection.Commit();
}
catch (Exception ex)
{
connection.Rollback();
throw new Exception($"Sync failed: {ex.Message}");
}
return new SyncResult
{
success = true,
sceneName = scene.name,
sceneId = sceneId,
syncedObjects = syncedObjects,
syncedComponents = syncedComponents,
closureRecords = closureRecords,
message = $"Synced {syncedObjects} objects, {syncedComponents} components, {closureRecords} closure records"
};
}
/// <summary>
/// Sync specific GameObject to database
/// </summary>
private object HandleSyncGameObject(JsonRpcRequest request)
{
var param = ValidateParam<SyncGameObjectParams>(request, "target");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var obj = FindGameObject(param.target);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.target}");
}
var scene = obj.scene;
int sceneId = EnsureSceneRecord(connection, scene);
var objectIdMap = new Dictionary<int, int>();
int objectId = SyncGameObject(connection, obj, sceneId, objectIdMap);
objectIdMap[obj.GetInstanceID()] = objectId;
int syncedComponents = 0;
if (param.includeComponents)
{
syncedComponents = SyncComponents(connection, obj, objectId);
}
// Sync hierarchy if requested
int syncedChildren = 0;
if (param.includeChildren)
{
var children = new List<GameObject>();
CollectGameObjectsRecursive(obj, children);
foreach (var child in children)
{
if (child != obj) // Skip self
{
int childId = SyncGameObject(connection, child, sceneId, objectIdMap);
objectIdMap[child.GetInstanceID()] = childId;
syncedChildren++;
if (param.includeComponents)
{
syncedComponents += SyncComponents(connection, child, childId);
}
}
}
}
return new SyncGameObjectResult
{
success = true,
objectName = obj.name,
objectId = objectId,
syncedComponents = syncedComponents,
syncedChildren = syncedChildren,
message = $"Synced '{obj.name}' with {syncedComponents} components and {syncedChildren} children"
};
}
/// <summary>
/// Get sync status
/// </summary>
private object HandleGetSyncStatus(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var scene = SceneManager.GetActiveScene();
// Count objects in scene
var allObjects = scene.GetRootGameObjects();
var objectList = new List<GameObject>();
foreach (var root in allObjects)
{
CollectGameObjectsRecursive(root, objectList);
}
int unityObjectCount = objectList.Count;
// Count objects in DB
var sceneIdSql = "SELECT scene_id FROM scenes WHERE scene_path = ?";
var sceneIds = connection.Query<SceneIdRecord>(sceneIdSql, scene.path);
int dbObjectCount = 0;
int dbComponentCount = 0;
int closureRecordCount = 0;
if (sceneIds.Count() > 0)
{
int sceneId = sceneIds.First().scene_id;
var objCountSql = "SELECT COUNT(*) FROM gameobjects WHERE scene_id = ? AND is_deleted = 0";
dbObjectCount = connection.ExecuteScalar<int>(objCountSql, sceneId);
var compCountSql = "SELECT COUNT(*) FROM components WHERE object_id IN (SELECT object_id FROM gameobjects WHERE scene_id = ?)";
dbComponentCount = connection.ExecuteScalar<int>(compCountSql, sceneId);
var closureSql = "SELECT COUNT(*) FROM gameobject_closure WHERE descendant_id IN (SELECT object_id FROM gameobjects WHERE scene_id = ?)";
closureRecordCount = connection.ExecuteScalar<int>(closureSql, sceneId);
}
return new SyncStatusResult
{
success = true,
sceneName = scene.name,
unityObjectCount = unityObjectCount,
dbObjectCount = dbObjectCount,
dbComponentCount = dbComponentCount,
closureRecordCount = closureRecordCount,
inSync = unityObjectCount == dbObjectCount
};
}
/// <summary>
/// Clear sync data
/// </summary>
private object HandleClearSync(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var scene = SceneManager.GetActiveScene();
var sceneIdSql = "SELECT scene_id FROM scenes WHERE scene_path = ?";
var sceneIds = connection.Query<SceneIdRecord>(sceneIdSql, scene.path);
if (sceneIds.Count() == 0)
{
return new ClearSyncResult
{
success = true,
deletedObjects = 0,
deletedComponents = 0,
message = "No sync data found for current scene"
};
}
int sceneId = sceneIds.First().scene_id;
// Delete in correct order (foreign keys)
connection.Execute("DELETE FROM gameobject_closure WHERE descendant_id IN (SELECT object_id FROM gameobjects WHERE scene_id = ?)", sceneId);
int deletedComponents = connection.Execute("DELETE FROM components WHERE object_id IN (SELECT object_id FROM gameobjects WHERE scene_id = ?)", sceneId);
int deletedObjects = connection.Execute("DELETE FROM gameobjects WHERE scene_id = ?", sceneId);
return new ClearSyncResult
{
success = true,
deletedObjects = deletedObjects,
deletedComponents = deletedComponents,
message = $"Deleted {deletedObjects} objects and {deletedComponents} components from database"
};
}
/// <summary>
/// Start automatic synchronization
/// </summary>
private object HandleStartAutoSync(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
try
{
var syncManager = DatabaseManager.Instance.SyncManager;
if (syncManager == null)
{
throw new Exception("SyncManager is not initialized");
}
if (syncManager.IsRunning)
{
return new AutoSyncResult
{
success = true,
message = "Auto-sync is already running",
isRunning = true
};
}
syncManager.StartSync();
return new AutoSyncResult
{
success = true,
message = "Auto-sync started successfully",
isRunning = true
};
}
catch (Exception ex)
{
ToolkitLogger.LogError("SyncHandler", $"Failed to start auto-sync: {ex.Message}");
throw new Exception($"Failed to start auto-sync: {ex.Message}");
}
}
/// <summary>
/// Stop automatic synchronization
/// </summary>
private object HandleStopAutoSync(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
try
{
var syncManager = DatabaseManager.Instance.SyncManager;
if (syncManager == null)
{
throw new Exception("SyncManager is not initialized");
}
if (!syncManager.IsRunning)
{
return new AutoSyncResult
{
success = true,
message = "Auto-sync is not running",
isRunning = false
};
}
syncManager.StopSync();
return new AutoSyncResult
{
success = true,
message = "Auto-sync stopped successfully",
isRunning = false
};
}
catch (Exception ex)
{
ToolkitLogger.LogError("SyncHandler", $"Failed to stop auto-sync: {ex.Message}");
throw new Exception($"Failed to stop auto-sync: {ex.Message}");
}
}
/// <summary>
/// Get automatic synchronization status
/// </summary>
private object HandleGetAutoSyncStatus(JsonRpcRequest request)
{
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
try
{
var syncManager = DatabaseManager.Instance.SyncManager;
if (syncManager == null)
{
return new AutoSyncStatusResult
{
success = true,
isRunning = false,
isInitialized = false,
lastSyncTime = null,
successfulSyncCount = 0,
failedSyncCount = 0,
syncIntervalMs = 0,
batchSize = 0
};
}
var healthStatus = syncManager.GetHealthStatus();
return new AutoSyncStatusResult
{
success = true,
isRunning = healthStatus.IsRunning,
isInitialized = true,
lastSyncTime = healthStatus.LastSyncTime == DateTime.MinValue ? null : healthStatus.LastSyncTime.ToString("yyyy-MM-dd HH:mm:ss"),
successfulSyncCount = healthStatus.SuccessfulSyncCount,
failedSyncCount = healthStatus.FailedSyncCount,
syncIntervalMs = healthStatus.SyncIntervalMs,
batchSize = healthStatus.BatchSize
};
}
catch (Exception ex)
{
ToolkitLogger.LogError("SyncHandler", $"Failed to get auto-sync status: {ex.Message}");
throw new Exception($"Failed to get auto-sync status: {ex.Message}");
}
}
#region Helper Methods
private void CollectGameObjectsRecursive(GameObject obj, List<GameObject> list)
{
list.Add(obj);
for (int i = 0; i < obj.transform.childCount; i++)
{
CollectGameObjectsRecursive(obj.transform.GetChild(i).gameObject, list);
}
}
private int EnsureSceneRecord(SQLite.SQLiteConnection connection, Scene scene)
{
var checkSql = "SELECT scene_id FROM scenes WHERE scene_path = ?";
var ids = connection.Query<SceneIdRecord>(checkSql, scene.path);
if (ids.Count() > 0)
{
// Update is_loaded status
connection.Execute("UPDATE scenes SET is_loaded = 1, updated_at = datetime('now', 'localtime') WHERE scene_id = ?", ids.First().scene_id);
return ids.First().scene_id;
}
var insertSql = @"
INSERT INTO scenes (scene_name, scene_path, build_index, is_loaded, created_at, updated_at)
VALUES (?, ?, ?, 1, datetime('now', 'localtime'), datetime('now', 'localtime'))
";
connection.Execute(insertSql, scene.name, scene.path, scene.buildIndex);
return connection.ExecuteScalar<int>("SELECT last_insert_rowid()");
}
private int SyncGameObject(SQLite.SQLiteConnection connection, GameObject obj, int sceneId, Dictionary<int, int> objectIdMap)
{
int instanceId = obj.GetInstanceID();
// Check if exists
var checkSql = "SELECT object_id FROM gameobjects WHERE instance_id = ?";
var existingIds = connection.Query<ObjectIdRecord>(checkSql, instanceId);
int? parentObjectId = null;
if (obj.transform.parent != null)
{
int parentInstanceId = obj.transform.parent.gameObject.GetInstanceID();
if (objectIdMap.ContainsKey(parentInstanceId))
{
parentObjectId = objectIdMap[parentInstanceId];
}
}
if (existingIds.Count() > 0)
{
// Update existing
var updateSql = @"
UPDATE gameobjects SET
scene_id = ?,
object_name = ?,
parent_id = ?,
tag = ?,
layer = ?,
is_active = ?,
is_static = ?,
is_deleted = 0,
updated_at = datetime('now', 'localtime')
WHERE object_id = ?
";
connection.Execute(updateSql, sceneId, obj.name, parentObjectId, obj.tag, obj.layer,
obj.activeSelf ? 1 : 0, obj.isStatic ? 1 : 0, existingIds.First().object_id);
return existingIds.First().object_id;
}
else
{
// Insert new
var insertSql = @"
INSERT INTO gameobjects (
instance_id, scene_id, object_name, parent_id, tag, layer,
is_active, is_static, is_deleted, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, datetime('now', 'localtime'), datetime('now', 'localtime'))
";
connection.Execute(insertSql, instanceId, sceneId, obj.name, parentObjectId, obj.tag, obj.layer,
obj.activeSelf ? 1 : 0, obj.isStatic ? 1 : 0);
return connection.ExecuteScalar<int>("SELECT last_insert_rowid()");
}
}
private int SyncComponents(SQLite.SQLiteConnection connection, GameObject obj, int objectId)
{
// Clear existing components for this object
connection.Execute("DELETE FROM components WHERE object_id = ?", objectId);
var components = obj.GetComponents<Component>();
int count = 0;
foreach (var comp in components)
{
if (comp == null) continue;
string componentType = comp.GetType().Name;
string componentData = "{}"; // Simple placeholder for now
var insertSql = @"
INSERT INTO components (object_id, component_type, component_data, is_enabled, created_at, updated_at)
VALUES (?, ?, ?, 1, datetime('now', 'localtime'), datetime('now', 'localtime'))
";
connection.Execute(insertSql, objectId, componentType, componentData);
count++;
}
return count;
}
private int BuildClosureTable(SQLite.SQLiteConnection connection, List<GameObject> objects, Dictionary<int, int> objectIdMap)
{
// Clear existing closure records for these objects
var objectIds = objectIdMap.Values.ToList();
if (objectIds.Count > 0)
{
// Use parameterized query to prevent SQL injection
// For small lists, delete individually
if (objectIds.Count <= 100)
{
foreach (var id in objectIds)
{
connection.Execute("DELETE FROM gameobject_closure WHERE descendant_id = ?", id);
}
}
else
{
// For large lists, use temporary table
connection.Execute("CREATE TEMP TABLE IF NOT EXISTS temp_object_ids (id INTEGER)");
connection.BeginTransaction();
try
{
foreach (var id in objectIds)
{
connection.Execute("INSERT INTO temp_object_ids VALUES (?)", id);
}
connection.Execute(@"
DELETE FROM gameobject_closure
WHERE descendant_id IN (SELECT id FROM temp_object_ids)
");
connection.Execute("DELETE FROM temp_object_ids");
connection.Commit();
}
catch
{
connection.Rollback();
throw;
}
}
}
int count = 0;
foreach (var obj in objects)
{
int objectId = objectIdMap[obj.GetInstanceID()];
// Add self-reference (depth = 0)
connection.Execute("INSERT INTO gameobject_closure (ancestor_id, descendant_id, depth) VALUES (?, ?, 0)", objectId, objectId);
count++;
// Add ancestors
var current = obj.transform.parent;
int depth = 1;
while (current != null)
{
int ancestorInstanceId = current.gameObject.GetInstanceID();
if (objectIdMap.ContainsKey(ancestorInstanceId))
{
int ancestorId = objectIdMap[ancestorInstanceId];
connection.Execute("INSERT INTO gameobject_closure (ancestor_id, descendant_id, depth) VALUES (?, ?, ?)",
ancestorId, objectId, depth);
count++;
}
current = current.parent;
depth++;
}
}
return count;
}
#endregion
#region Data Classes
[Serializable]
public class SyncSceneParams
{
public bool clearExisting = true;
public bool includeComponents = true;
public bool buildClosure = true;
}
[Serializable]
public class SyncGameObjectParams
{
public string target;
public bool includeComponents = true;
public bool includeChildren = false;
}
private class SceneIdRecord
{
public int scene_id { get; set; }
}
private class ObjectIdRecord
{
public int object_id { get; set; }
}
[Serializable]
public class SyncResult
{
public bool success;
public string sceneName;
public int sceneId;
public int syncedObjects;
public int syncedComponents;
public int closureRecords;
public string message;
}
[Serializable]
public class SyncGameObjectResult
{
public bool success;
public string objectName;
public int objectId;
public int syncedComponents;
public int syncedChildren;
public string message;
}
[Serializable]
public class SyncStatusResult
{
public bool success;
public string sceneName;
public int unityObjectCount;
public int dbObjectCount;
public int dbComponentCount;
public int closureRecordCount;
public bool inSync;
}
[Serializable]
public class ClearSyncResult
{
public bool success;
public int deletedObjects;
public int deletedComponents;
public string message;
}
[Serializable]
public class AutoSyncResult
{
public bool success;
public string message;
public bool isRunning;
}
[Serializable]
public class AutoSyncStatusResult
{
public bool success;
public bool isRunning;
public bool isInitialized;
public string lastSyncTime;
public int successfulSyncCount;
public int failedSyncCount;
public int syncIntervalMs;
public int batchSize;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,276 @@
using System;
using UnityEngine;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Database;
using UnityEditorToolkit.Editor.Database.Commands;
using Cysharp.Threading.Tasks;
using UnityEditorToolkit.Editor.Utils;
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);
// Save old values for Command Pattern
var oldPosition = transform.position;
var oldRotation = transform.rotation;
var oldScale = transform.localScale;
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(transform, "Set Position");
#endif
// ✅ ToVector3() 사용 (유효성 검증 포함)
var newPosition = param.position.ToVector3();
transform.position = newPosition;
// Execute Command Pattern (if database is connected)
ExecuteTransformCommandAsync(
transform.gameObject,
oldPosition, oldRotation, oldScale,
newPosition, oldRotation, oldScale
).Forget();
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);
// Save old values for Command Pattern
var oldPosition = transform.position;
var oldRotation = transform.rotation;
var oldScale = transform.localScale;
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(transform, "Set Rotation");
#endif
// ✅ ToVector3() 사용 (유효성 검증 포함)
transform.eulerAngles = param.rotation.ToVector3();
var newRotation = transform.rotation; // Quaternion으로 가져오기
// Execute Command Pattern (if database is connected)
ExecuteTransformCommandAsync(
transform.gameObject,
oldPosition, oldRotation, oldScale,
oldPosition, newRotation, oldScale
).Forget();
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);
// Save old values for Command Pattern
var oldPosition = transform.position;
var oldRotation = transform.rotation;
var oldScale = transform.localScale;
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(transform, "Set Scale");
#endif
// ✅ ToVector3() 사용 (유효성 검증 포함)
var newScale = param.scale.ToVector3();
transform.localScale = newScale;
// Execute Command Pattern (if database is connected)
ExecuteTransformCommandAsync(
transform.gameObject,
oldPosition, oldRotation, oldScale,
oldPosition, oldRotation, newScale
).Forget();
return new { success = true };
}
/// <summary>
/// Execute TransformChangeCommand asynchronously (database persistence)
/// </summary>
private async UniTaskVoid ExecuteTransformCommandAsync(
GameObject gameObject,
Vector3 oldPosition, Quaternion oldRotation, Vector3 oldScale,
Vector3 newPosition, Quaternion newRotation, Vector3 newScale)
{
try
{
#if UNITY_EDITOR
// Check if database is connected
if (DatabaseManager.Instance == null ||
!DatabaseManager.Instance.IsConnected ||
DatabaseManager.Instance.CommandHistory == null)
{
return;
}
// Create command
var command = new TransformChangeCommand(
gameObject,
oldPosition, oldRotation, oldScale,
newPosition, newRotation, newScale
);
// Execute through CommandHistory (async, database persistence)
await DatabaseManager.Instance.CommandHistory.ExecuteCommandAsync(command);
#endif
}
catch (Exception ex)
{
ToolkitLogger.LogWarning("TransformHandler", $"Command execution failed: {ex.Message}");
}
}
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: 4a496e0fc31b7a749bc8d1f5879826d9

View File

@@ -0,0 +1,614 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Database;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Transform History commands (Track and restore transform changes)
/// </summary>
public class TransformHistoryHandler : BaseHandler
{
public override string Category => "TransformHistory";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Record":
return HandleRecord(request);
case "List":
return HandleList(request);
case "Restore":
return HandleRestore(request);
case "Compare":
return HandleCompare(request);
case "Clear":
return HandleClear(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
/// <summary>
/// Record current transform state for a GameObject
/// </summary>
private object HandleRecord(JsonRpcRequest request)
{
var param = ValidateParam<RecordParams>(request, "target");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
// Find GameObject
var obj = FindGameObject(param.target);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.target}");
}
// Ensure GameObject record exists
int objectId = EnsureGameObjectRecord(connection, obj);
// Record transform
var transform = obj.transform;
var sql = @"
INSERT INTO transforms (
object_id,
position_x, position_y, position_z,
rotation_x, rotation_y, rotation_z, rotation_w,
scale_x, scale_y, scale_z,
recorded_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))
";
connection.Execute(sql,
objectId,
transform.localPosition.x, transform.localPosition.y, transform.localPosition.z,
transform.localRotation.x, transform.localRotation.y, transform.localRotation.z, transform.localRotation.w,
transform.localScale.x, transform.localScale.y, transform.localScale.z
);
// Get inserted ID
var transformId = connection.ExecuteScalar<int>("SELECT last_insert_rowid()");
return new RecordResult
{
success = true,
transformId = transformId,
objectId = objectId,
objectName = obj.name,
position = new Vector3Data(transform.localPosition),
rotation = new Vector4Data(transform.localRotation),
scale = new Vector3Data(transform.localScale),
message = $"Transform recorded for '{obj.name}'"
};
}
/// <summary>
/// List transform history for a GameObject
/// </summary>
private object HandleList(JsonRpcRequest request)
{
var param = ValidateParam<ListParams>(request, "target");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
// Find GameObject
var obj = FindGameObject(param.target);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.target}");
}
// Get object ID
var checkSql = "SELECT object_id FROM gameobjects WHERE instance_id = ?";
var ids = connection.Query<ObjectIdRecord>(checkSql, obj.GetInstanceID());
if (ids.Count() == 0)
{
return new ListResult
{
success = true,
objectName = obj.name,
count = 0,
history = new List<TransformHistoryEntry>()
};
}
int objectId = ids[0].object_id;
// Get history
var sql = @"
SELECT transform_id, object_id,
position_x, position_y, position_z,
rotation_x, rotation_y, rotation_z, rotation_w,
scale_x, scale_y, scale_z,
recorded_at
FROM transforms
WHERE object_id = ?
ORDER BY recorded_at DESC
LIMIT ?
";
var records = connection.Query<TransformRecord>(sql, objectId, param.limit);
var history = records.Select(r => new TransformHistoryEntry
{
transformId = r.transform_id,
position = new Vector3Data(r.position_x, r.position_y, r.position_z),
rotation = new Vector4Data(r.rotation_x, r.rotation_y, r.rotation_z, r.rotation_w),
scale = new Vector3Data(r.scale_x, r.scale_y, r.scale_z),
recordedAt = r.recorded_at
}).ToList();
return new ListResult
{
success = true,
objectName = obj.name,
objectId = objectId,
count = history.Count,
history = history
};
}
/// <summary>
/// Restore transform from history
/// </summary>
private object HandleRestore(JsonRpcRequest request)
{
var param = ValidateParam<RestoreParams>(request, "transformId");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
// Get transform record
var sql = @"
SELECT t.transform_id, t.object_id,
t.position_x, t.position_y, t.position_z,
t.rotation_x, t.rotation_y, t.rotation_z, t.rotation_w,
t.scale_x, t.scale_y, t.scale_z,
t.recorded_at,
g.instance_id, g.object_name
FROM transforms t
INNER JOIN gameobjects g ON t.object_id = g.object_id
WHERE t.transform_id = ?
";
var records = connection.Query<TransformWithObjectRecord>(sql, param.transformId);
if (records.Count() == 0)
{
throw new Exception($"Transform record {param.transformId} not found");
}
var record = records[0];
// Find GameObject by instance ID
#if UNITY_EDITOR
var obj = EditorUtility.InstanceIDToObject(record.instance_id) as GameObject;
#else
var obj = FindGameObject(record.object_name);
#endif
if (obj == null)
{
throw new Exception($"GameObject '{record.object_name}' not found in scene");
}
// Record undo
#if UNITY_EDITOR
Undo.RecordObject(obj.transform, "Restore Transform from History");
#endif
// Restore transform
obj.transform.localPosition = new Vector3(record.position_x, record.position_y, record.position_z);
obj.transform.localRotation = new Quaternion(record.rotation_x, record.rotation_y, record.rotation_z, record.rotation_w);
obj.transform.localScale = new Vector3(record.scale_x, record.scale_y, record.scale_z);
return new RestoreResult
{
success = true,
transformId = record.transform_id,
objectName = record.object_name,
position = new Vector3Data(obj.transform.localPosition),
rotation = new Vector4Data(obj.transform.localRotation),
scale = new Vector3Data(obj.transform.localScale),
message = $"Transform restored for '{record.object_name}' from {record.recorded_at}"
};
}
/// <summary>
/// Compare two transform records
/// </summary>
private object HandleCompare(JsonRpcRequest request)
{
var param = ValidateParam<CompareParams>(request, "transformId1");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
var sql = @"
SELECT transform_id, object_id,
position_x, position_y, position_z,
rotation_x, rotation_y, rotation_z, rotation_w,
scale_x, scale_y, scale_z,
recorded_at
FROM transforms
WHERE transform_id IN (?, ?)
";
var records = connection.Query<TransformRecord>(sql, param.transformId1, param.transformId2);
if (records.Count() < 2)
{
throw new Exception("One or both transform records not found");
}
var t1 = records.First(r => r.transform_id == param.transformId1);
var t2 = records.First(r => r.transform_id == param.transformId2);
var positionDiff = new Vector3(
t2.position_x - t1.position_x,
t2.position_y - t1.position_y,
t2.position_z - t1.position_z
);
var scaleDiff = new Vector3(
t2.scale_x - t1.scale_x,
t2.scale_y - t1.scale_y,
t2.scale_z - t1.scale_z
);
var rot1 = new Quaternion(t1.rotation_x, t1.rotation_y, t1.rotation_z, t1.rotation_w);
var rot2 = new Quaternion(t2.rotation_x, t2.rotation_y, t2.rotation_z, t2.rotation_w);
var rotationAngle = Quaternion.Angle(rot1, rot2);
return new CompareResult
{
success = true,
transform1 = new TransformHistoryEntry
{
transformId = t1.transform_id,
position = new Vector3Data(t1.position_x, t1.position_y, t1.position_z),
rotation = new Vector4Data(t1.rotation_x, t1.rotation_y, t1.rotation_z, t1.rotation_w),
scale = new Vector3Data(t1.scale_x, t1.scale_y, t1.scale_z),
recordedAt = t1.recorded_at
},
transform2 = new TransformHistoryEntry
{
transformId = t2.transform_id,
position = new Vector3Data(t2.position_x, t2.position_y, t2.position_z),
rotation = new Vector4Data(t2.rotation_x, t2.rotation_y, t2.rotation_z, t2.rotation_w),
scale = new Vector3Data(t2.scale_x, t2.scale_y, t2.scale_z),
recordedAt = t2.recorded_at
},
positionDifference = new Vector3Data(positionDiff),
rotationAngleDifference = rotationAngle,
scaleDifference = new Vector3Data(scaleDiff)
};
}
/// <summary>
/// Clear transform history for a GameObject
/// </summary>
private object HandleClear(JsonRpcRequest request)
{
var param = ValidateParam<ClearParams>(request, "target");
if (!DatabaseManager.Instance.IsConnected)
{
throw new Exception("Database is not connected");
}
var connection = DatabaseManager.Instance.Connector?.Connection;
if (connection == null)
{
throw new Exception("Failed to get database connection");
}
// Find GameObject
var obj = FindGameObject(param.target);
if (obj == null)
{
throw new Exception($"GameObject not found: {param.target}");
}
// Get object ID
var checkSql = "SELECT object_id FROM gameobjects WHERE instance_id = ?";
var ids = connection.Query<ObjectIdRecord>(checkSql, obj.GetInstanceID());
if (ids.Count() == 0)
{
return new ClearResult
{
success = true,
objectName = obj.name,
deletedCount = 0,
message = $"No transform history found for '{obj.name}'"
};
}
int objectId = ids[0].object_id;
// Delete history
var deleteSql = "DELETE FROM transforms WHERE object_id = ?";
connection.Execute(deleteSql, objectId);
// Get count
var countSql = "SELECT changes()";
var deletedCount = connection.ExecuteScalar<int>(countSql);
return new ClearResult
{
success = true,
objectName = obj.name,
objectId = objectId,
deletedCount = deletedCount,
message = $"Cleared {deletedCount} transform records for '{obj.name}'"
};
}
#region Helper Methods
private int EnsureGameObjectRecord(SQLite.SQLiteConnection connection, GameObject obj)
{
var checkSql = "SELECT object_id FROM gameobjects WHERE instance_id = ?";
var ids = connection.Query<ObjectIdRecord>(checkSql, obj.GetInstanceID());
if (ids.Count() > 0)
{
return ids[0].object_id;
}
// Get scene ID
int sceneId = EnsureSceneRecord(connection, obj.scene);
// Insert GameObject record
var insertSql = @"
INSERT INTO gameobjects (
instance_id, scene_id, object_name,
tag, layer, is_active, is_static, is_deleted,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, datetime('now', 'localtime'), datetime('now', 'localtime'))
";
connection.Execute(insertSql,
obj.GetInstanceID(),
sceneId,
obj.name,
obj.tag,
obj.layer,
obj.activeSelf ? 1 : 0,
obj.isStatic ? 1 : 0
);
return connection.ExecuteScalar<int>("SELECT last_insert_rowid()");
}
private int EnsureSceneRecord(SQLite.SQLiteConnection connection, UnityEngine.SceneManagement.Scene scene)
{
var checkSql = "SELECT scene_id FROM scenes WHERE scene_path = ?";
var ids = connection.Query<SceneIdRecord>(checkSql, scene.path);
if (ids.Count() > 0)
{
return ids[0].scene_id;
}
var insertSql = @"
INSERT INTO scenes (scene_name, scene_path, build_index, is_loaded, created_at, updated_at)
VALUES (?, ?, ?, 1, datetime('now', 'localtime'), datetime('now', 'localtime'))
";
connection.Execute(insertSql, scene.name, scene.path, scene.buildIndex);
return connection.ExecuteScalar<int>("SELECT last_insert_rowid()");
}
#endregion
#region Data Classes
// Parameter classes
[Serializable]
public class RecordParams
{
public string target;
}
[Serializable]
public class ListParams
{
public string target;
public int limit = 50;
}
[Serializable]
public class RestoreParams
{
public int transformId;
}
[Serializable]
public class CompareParams
{
public int transformId1;
public int transformId2;
}
[Serializable]
public class ClearParams
{
public string target;
}
// Database record classes
private class ObjectIdRecord
{
public int object_id { get; set; }
}
private class SceneIdRecord
{
public int scene_id { get; set; }
}
private class TransformRecord
{
public int transform_id { get; set; }
public int object_id { get; set; }
public float position_x { get; set; }
public float position_y { get; set; }
public float position_z { get; set; }
public float rotation_x { get; set; }
public float rotation_y { get; set; }
public float rotation_z { get; set; }
public float rotation_w { get; set; }
public float scale_x { get; set; }
public float scale_y { get; set; }
public float scale_z { get; set; }
public string recorded_at { get; set; }
}
private class TransformWithObjectRecord : TransformRecord
{
public int instance_id { get; set; }
public string object_name { get; set; }
}
// Result classes
[Serializable]
public class RecordResult
{
public bool success;
public int transformId;
public int objectId;
public string objectName;
public Vector3Data position;
public Vector4Data rotation;
public Vector3Data scale;
public string message;
}
[Serializable]
public class ListResult
{
public bool success;
public string objectName;
public int objectId;
public int count;
public List<TransformHistoryEntry> history;
}
[Serializable]
public class TransformHistoryEntry
{
public int transformId;
public Vector3Data position;
public Vector4Data rotation;
public Vector3Data scale;
public string recordedAt;
}
[Serializable]
public class RestoreResult
{
public bool success;
public int transformId;
public string objectName;
public Vector3Data position;
public Vector4Data rotation;
public Vector3Data scale;
public string message;
}
[Serializable]
public class CompareResult
{
public bool success;
public TransformHistoryEntry transform1;
public TransformHistoryEntry transform2;
public Vector3Data positionDifference;
public float rotationAngleDifference;
public Vector3Data scaleDifference;
}
[Serializable]
public class ClearResult
{
public bool success;
public string objectName;
public int objectId;
public int deletedCount;
public string message;
}
// Vector data classes
[Serializable]
public class Vector3Data
{
public float x;
public float y;
public float z;
public Vector3Data() { }
public Vector3Data(Vector3 v) { x = v.x; y = v.y; z = v.z; }
public Vector3Data(float x, float y, float z) { this.x = x; this.y = y; this.z = z; }
}
[Serializable]
public class Vector4Data
{
public float x;
public float y;
public float z;
public float w;
public Vector4Data() { }
public Vector4Data(Quaternion q) { x = q.x; y = q.y; z = q.z; w = q.w; }
public Vector4Data(float x, float y, float z, float w) { this.x = x; this.y = y; this.z = z; this.w = w; }
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,262 @@
using System;
using UnityEngine;
using UnityEditorToolkit.Protocol;
using UnityEditorToolkit.Editor.Utils;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
namespace UnityEditorToolkit.Handlers
{
/// <summary>
/// Handler for Wait commands
/// Supports waiting for compilation, play mode changes, scene loads, and sleep
/// </summary>
public class WaitHandler : BaseHandler
{
public override string Category => "Wait";
protected override object HandleMethod(string method, JsonRpcRequest request)
{
switch (method)
{
case "Wait":
return HandleWait(request);
default:
throw new Exception($"Unknown method: {method}");
}
}
private object HandleWait(JsonRpcRequest request)
{
#if UNITY_EDITOR
var param = ValidateParam<WaitParams>(request, "type");
if (string.IsNullOrWhiteSpace(param.type))
{
throw new Exception("Wait type is required");
}
// Get send callback from request context
var sendCallback = request.GetContext<Action<string>>("sendCallback");
if (sendCallback == null)
{
throw new Exception("Send callback not found in request context");
}
double timeout = param.timeout > 0 ? param.timeout : 300.0;
// Convert request ID to string for ResponseQueue
string requestId = request.Id?.ToString() ?? string.Empty;
switch (param.type.ToLower())
{
case "compile":
return HandleWaitCompile(requestId, sendCallback, timeout);
case "playmode":
return HandleWaitPlayMode(requestId, sendCallback, param.value, timeout);
case "sleep":
return HandleWaitSleep(requestId, sendCallback, param.seconds, timeout);
case "scene":
return HandleWaitScene(requestId, sendCallback, timeout);
default:
throw new Exception($"Unknown wait type: {param.type}. Supported types: compile, playmode, sleep, scene");
}
#else
throw new Exception("Wait is only available in Unity Editor");
#endif
}
#if UNITY_EDITOR
private object HandleWaitCompile(string requestId, Action<string> sendCallback, double timeout)
{
ToolkitLogger.Log("WaitHandler", "Waiting for compilation to complete...");
ResponseQueue.Instance.Register(
requestId,
condition: () => !EditorApplication.isCompiling,
resultProvider: () => new
{
success = true,
type = "compile",
message = "Compilation completed"
},
sendCallback,
timeout
);
// Return null to indicate delayed response
return null;
}
private object HandleWaitPlayMode(string requestId, Action<string> sendCallback, string targetState, double timeout)
{
if (string.IsNullOrWhiteSpace(targetState))
{
throw new Exception("PlayMode target state is required (enter, exit, or pause)");
}
bool targetIsPlaying;
bool targetIsPaused = false;
string stateDescription;
switch (targetState.ToLower())
{
case "enter":
targetIsPlaying = true;
stateDescription = "entered play mode";
break;
case "exit":
targetIsPlaying = false;
stateDescription = "exited play mode";
break;
case "pause":
targetIsPlaying = true;
targetIsPaused = true;
stateDescription = "paused play mode";
break;
default:
throw new Exception($"Invalid playmode state: {targetState}. Use 'enter', 'exit', or 'pause'");
}
ToolkitLogger.Log("WaitHandler", $"Waiting for play mode to {stateDescription}...");
ResponseQueue.Instance.Register(
requestId,
condition: () =>
{
if (targetIsPaused)
{
return EditorApplication.isPlaying && EditorApplication.isPaused;
}
return EditorApplication.isPlaying == targetIsPlaying;
},
resultProvider: () => new
{
success = true,
type = "playmode",
state = targetState,
message = $"Play mode {stateDescription}"
},
sendCallback,
timeout
);
return null;
}
private object HandleWaitSleep(string requestId, Action<string> sendCallback, double seconds, double timeout)
{
if (seconds <= 0)
{
throw new Exception("Sleep duration must be greater than 0");
}
if (seconds > timeout)
{
throw new Exception($"Sleep duration ({seconds}s) exceeds timeout ({timeout}s)");
}
ToolkitLogger.Log("WaitHandler", $"Sleeping for {seconds} seconds...");
double wakeTime = EditorApplication.timeSinceStartup + seconds;
ResponseQueue.Instance.Register(
requestId,
condition: () => EditorApplication.timeSinceStartup >= wakeTime,
resultProvider: () => new
{
success = true,
type = "sleep",
seconds = seconds,
message = $"Slept for {seconds} seconds"
},
sendCallback,
timeout
);
return null;
}
private object HandleWaitScene(string requestId, Action<string> sendCallback, double timeout)
{
ToolkitLogger.Log("WaitHandler", "Waiting for scene to finish loading...");
// Check if in play mode
if (!EditorApplication.isPlaying)
{
throw new Exception("Scene loading wait is only available in play mode");
}
// Record initial state
var initialSceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
var initialSceneCount = UnityEngine.SceneManagement.SceneManager.sceneCount;
var startTime = EditorApplication.timeSinceStartup;
ToolkitLogger.LogDebug("WaitHandler", $"Initial scene: {initialSceneName}, scene count: {initialSceneCount}");
ResponseQueue.Instance.Register(
requestId,
condition: () =>
{
// Wait conditions:
// 1. Not compiling (compilation would reload domain)
// 2. Not paused (scene is actively running)
// 3. At least 0.1 seconds elapsed (give scene time to initialize)
// 4. Scene is loaded (isLoaded = true)
if (EditorApplication.isCompiling)
return false;
if (EditorApplication.isPaused)
return false;
// Minimum delay to ensure scene has initialized
double elapsed = EditorApplication.timeSinceStartup - startTime;
if (elapsed < 0.1)
return false;
// Check if active scene is loaded
var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
if (!activeScene.isLoaded)
return false;
return true;
},
resultProvider: () =>
{
var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
return new
{
success = true,
type = "scene",
sceneName = currentScene.name,
sceneCount = UnityEngine.SceneManagement.SceneManager.sceneCount,
message = "Scene loading completed"
};
},
sendCallback,
timeout
);
return null;
}
#endif
// Parameter classes
[Serializable]
public class WaitParams
{
public string type; // compile, playmode, sleep, scene
public string value; // for playmode: enter, exit, pause
public double seconds; // for sleep: duration in seconds
public double timeout = 300; // default 300 seconds
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 296f9db28b055034baedccbf47e6bfe7