From 1da7b24c8e78263cbd2508fd4c8db81c518e560d Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:19:28 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 1045 ++++++++++++ skills/SKILL.md | 387 +++++ skills/assets/unity-package/Editor.meta | 8 + .../unity-package/Editor/Attributes.meta | 8 + .../Attributes/ExecutableMethodAttribute.cs | 38 + .../ExecutableMethodAttribute.cs.meta | 2 + .../assets/unity-package/Editor/Database.meta | 8 + .../Editor/Database/Commands.meta | 8 + .../Editor/Database/Commands/CommandBase.cs | 157 ++ .../Database/Commands/CommandBase.cs.meta | 2 + .../Database/Commands/CommandFactory.cs | 58 + .../Database/Commands/CommandFactory.cs.meta | 2 + .../Database/Commands/CommandHistory.cs | 514 ++++++ .../Database/Commands/CommandHistory.cs.meta | 2 + .../Commands/CreateGameObjectCommand.cs | 187 +++ .../Commands/CreateGameObjectCommand.cs.meta | 2 + .../Commands/DeleteGameObjectCommand.cs | 156 ++ .../Commands/DeleteGameObjectCommand.cs.meta | 2 + .../Editor/Database/Commands/ICommand.cs | 57 + .../Editor/Database/Commands/ICommand.cs.meta | 2 + .../Commands/TransformChangeCommand.cs | 160 ++ .../Commands/TransformChangeCommand.cs.meta | 2 + .../Editor/Database/DatabaseConfig.cs | 347 ++++ .../Editor/Database/DatabaseConfig.cs.meta | 2 + .../Editor/Database/DatabaseManager.cs | 598 +++++++ .../Editor/Database/DatabaseManager.cs.meta | 2 + .../Editor/Database/MigrationRunner.cs | 596 +++++++ .../Editor/Database/MigrationRunner.cs.meta | 2 + .../Editor/Database/Migrations.meta | 8 + .../Migration_001_InitialSchema.sql | 358 ++++ .../Migration_001_InitialSchema.sql.meta | 7 + .../Migration_002_AddGameObjectGuid.sql | 31 + .../Migration_002_AddGameObjectGuid.sql.meta | 10 + .../unity-package/Editor/Database/Models.meta | 8 + .../Editor/Database/Models/.gitkeep | 0 .../Editor/Database/Queries.meta | 8 + .../Editor/Database/Queries/.gitkeep | 0 .../Editor/Database/SQLiteConnector.cs | 344 ++++ .../Editor/Database/SQLiteConnector.cs.meta | 2 + .../unity-package/Editor/Database/Setup.meta | 8 + .../Editor/Database/Setup/DatabaseCreator.cs | 182 +++ .../Database/Setup/DatabaseCreator.cs.meta | 2 + .../Database/Setup/DatabaseSetupWizard.cs | 222 +++ .../Setup/DatabaseSetupWizard.cs.meta | 2 + .../Editor/Database/SyncManager.cs | 866 ++++++++++ .../Editor/Database/SyncManager.cs.meta | 2 + .../Editor/DatabaseStatusWindow.cs | 371 +++++ .../Editor/DatabaseStatusWindow.cs.meta | 2 + .../Editor/DatabaseStatusWindow.uxml | 81 + .../Editor/DatabaseStatusWindow.uxml.meta | 10 + .../Editor/EditorServerCLIInstaller.cs | 542 +++++++ .../Editor/EditorServerCLIInstaller.cs.meta | 2 + .../Editor/EditorServerCommandRunner.cs | 213 +++ .../Editor/EditorServerCommandRunner.cs.meta | 2 + .../Editor/EditorServerPathManager.cs | 258 +++ .../Editor/EditorServerPathManager.cs.meta | 2 + .../Editor/EditorServerWindow.cs | 654 ++++++++ .../Editor/EditorServerWindow.cs.meta | 2 + .../Editor/EditorServerWindow.uss | 162 ++ .../Editor/EditorServerWindow.uss.meta | 11 + .../Editor/EditorServerWindow.uxml | 274 ++++ .../Editor/EditorServerWindow.uxml.meta | 10 + .../Editor/EditorServerWindowData.cs | 220 +++ .../Editor/EditorServerWindowData.cs.meta | 2 + .../Editor/EditorServerWindowDatabase.cs | 1305 +++++++++++++++ .../Editor/EditorServerWindowDatabase.cs.meta | 2 + .../Editor/EditorToolbarExtension.cs | 232 +++ .../Editor/EditorToolbarExtension.cs.meta | 2 + .../assets/unity-package/Editor/Handlers.meta | 8 + .../Editor/Handlers/AnalyticsHandler.cs | 511 ++++++ .../Editor/Handlers/AnalyticsHandler.cs.meta | 11 + .../Editor/Handlers/AnimationHandler.cs | 566 +++++++ .../Editor/Handlers/AnimationHandler.cs.meta | 2 + .../Editor/Handlers/AssetHandler.cs | 1434 +++++++++++++++++ .../Editor/Handlers/AssetHandler.cs.meta | 2 + .../Editor/Handlers/BaseHandler.cs | 210 +++ .../Editor/Handlers/BaseHandler.cs.meta | 2 + .../Editor/Handlers/ChainHandler.cs | 160 ++ .../Editor/Handlers/ChainHandler.cs.meta | 2 + .../Editor/Handlers/ComponentHandler.cs | 973 +++++++++++ .../Editor/Handlers/ComponentHandler.cs.meta | 2 + .../Editor/Handlers/ConsoleHandler.cs | 321 ++++ .../Editor/Handlers/ConsoleHandler.cs.meta | 2 + .../Editor/Handlers/DatabaseHandler.cs | 806 +++++++++ .../Editor/Handlers/DatabaseHandler.cs.meta | 2 + .../Editor/Handlers/EditorHandler.cs | 427 +++++ .../Editor/Handlers/EditorHandler.cs.meta | 2 + .../Editor/Handlers/GameObjectHandler.cs | 469 ++++++ .../Editor/Handlers/GameObjectHandler.cs.meta | 2 + .../Editor/Handlers/HierarchyHandler.cs | 103 ++ .../Editor/Handlers/HierarchyHandler.cs.meta | 2 + .../Editor/Handlers/MaterialHandler.cs | 618 +++++++ .../Editor/Handlers/MaterialHandler.cs.meta | 2 + .../Editor/Handlers/MenuHandler.cs | 280 ++++ .../Editor/Handlers/MenuHandler.cs.meta | 2 + .../Editor/Handlers/PrefabHandler.cs | 859 ++++++++++ .../Editor/Handlers/PrefabHandler.cs.meta | 2 + .../Editor/Handlers/PrefsHandler.cs | 443 +++++ .../Editor/Handlers/PrefsHandler.cs.meta | 2 + .../Editor/Handlers/SceneHandler.cs | 312 ++++ .../Editor/Handlers/SceneHandler.cs.meta | 2 + .../Editor/Handlers/SnapshotHandler.cs | 645 ++++++++ .../Editor/Handlers/SnapshotHandler.cs.meta | 2 + .../Editor/Handlers/SyncHandler.cs | 737 +++++++++ .../Editor/Handlers/SyncHandler.cs.meta | 2 + .../Editor/Handlers/TransformHandler.cs | 276 ++++ .../Editor/Handlers/TransformHandler.cs.meta | 2 + .../Handlers/TransformHistoryHandler.cs | 614 +++++++ .../Handlers/TransformHistoryHandler.cs.meta | 2 + .../Editor/Handlers/WaitHandler.cs | 262 +++ .../Editor/Handlers/WaitHandler.cs.meta | 2 + .../Editor/PackageInitializer.cs | 154 ++ .../Editor/PackageInitializer.cs.meta | 2 + .../assets/unity-package/Editor/Protocol.meta | 8 + .../Editor/Protocol/JsonRpcError.cs | 85 + .../Editor/Protocol/JsonRpcError.cs.meta | 2 + .../Editor/Protocol/JsonRpcRequest.cs | 108 ++ .../Editor/Protocol/JsonRpcRequest.cs.meta | 2 + .../Editor/Protocol/JsonRpcResponse.cs | 66 + .../Editor/Protocol/JsonRpcResponse.cs.meta | 2 + .../assets/unity-package/Editor/Server.meta | 8 + .../Editor/Server/EditorWebSocketServer.cs | 474 ++++++ .../Server/EditorWebSocketServer.cs.meta | 2 + .../Editor/Server/ServerStatus.cs | 199 +++ .../Editor/Server/ServerStatus.cs.meta | 2 + .../Editor/UnityEditorToolkit.Editor.asmdef | 20 + .../UnityEditorToolkit.Editor.asmdef.meta | 7 + skills/assets/unity-package/Editor/Utils.meta | 8 + .../Utils/EditorMainThreadDispatcher.cs | 134 ++ .../Utils/EditorMainThreadDispatcher.cs.meta | 2 + .../unity-package/Editor/Utils/Logger.cs | 129 ++ .../unity-package/Editor/Utils/Logger.cs.meta | 2 + .../Editor/Utils/ResponseQueue.cs | 221 +++ .../Editor/Utils/ResponseQueue.cs.meta | 2 + skills/assets/unity-package/LICENSE.md | 198 +++ skills/assets/unity-package/LICENSE.md.meta | 7 + skills/assets/unity-package/README.ko.md | 272 ++++ skills/assets/unity-package/README.ko.md.meta | 7 + skills/assets/unity-package/README.md | 272 ++++ skills/assets/unity-package/README.md.meta | 7 + skills/assets/unity-package/Runtime.meta | 8 + .../unity-package/Runtime/GameObjectGuid.cs | 111 ++ .../Runtime/GameObjectGuid.cs.meta | 2 + .../unity-package/Runtime/Handlers.meta | 8 + .../Runtime/Handlers/BaseHandler.cs | 209 +++ .../Runtime/Handlers/BaseHandler.cs.meta | 2 + .../Runtime/Handlers/ConsoleHandler.cs | 165 ++ .../Runtime/Handlers/ConsoleHandler.cs.meta | 2 + .../Runtime/Handlers/GameObjectHandler.cs | 171 ++ .../Handlers/GameObjectHandler.cs.meta | 2 + .../Runtime/Handlers/HierarchyHandler.cs | 103 ++ .../Runtime/Handlers/HierarchyHandler.cs.meta | 2 + .../Runtime/Handlers/SceneHandler.cs | 115 ++ .../Runtime/Handlers/SceneHandler.cs.meta | 2 + .../Runtime/Handlers/TransformHandler.cs | 197 +++ .../Runtime/Handlers/TransformHandler.cs.meta | 2 + .../unity-package/Runtime/Protocol.meta | 8 + .../Runtime/Protocol/JsonRpcError.cs | 85 + .../Runtime/Protocol/JsonRpcError.cs.meta | 2 + .../Runtime/Protocol/JsonRpcRequest.cs | 80 + .../Runtime/Protocol/JsonRpcRequest.cs.meta | 2 + .../Runtime/Protocol/JsonRpcResponse.cs | 66 + .../Runtime/Protocol/JsonRpcResponse.cs.meta | 2 + .../assets/unity-package/Runtime/Server.meta | 8 + .../Runtime/Server/ServerStatus.cs | 195 +++ .../Runtime/Server/ServerStatus.cs.meta | 2 + .../Runtime/Server/UnityEditorServer.cs | 380 +++++ .../Runtime/Server/UnityEditorServer.cs.meta | 2 + .../Runtime/UnityEditorToolkit.asmdef | 16 + .../Runtime/UnityEditorToolkit.asmdef.meta | 7 + .../assets/unity-package/Runtime/Utils.meta | 8 + .../Utils/UnityMainThreadDispatcher.cs | 151 ++ .../Utils/UnityMainThreadDispatcher.cs.meta | 2 + skills/assets/unity-package/Tests.meta | 8 + skills/assets/unity-package/Tests/Editor.meta | 8 + .../Tests/Editor/GameObjectCachingTests.cs | 297 ++++ .../Editor/GameObjectCachingTests.cs.meta | 2 + .../Tests/Editor/JsonRpcProtocolTests.cs | 363 +++++ .../Tests/Editor/JsonRpcProtocolTests.cs.meta | 2 + .../UnityEditorToolkit.Editor.Tests.asmdef | 26 + ...nityEditorToolkit.Editor.Tests.asmdef.meta | 7 + .../Editor/UnityMainThreadDispatcherTests.cs | 251 +++ .../UnityMainThreadDispatcherTests.cs.meta | 2 + .../Tests/Editor/Vector3ValidationTests.cs | 336 ++++ .../Editor/Vector3ValidationTests.cs.meta | 2 + .../assets/unity-package/Tests/Runtime.meta | 8 + .../Runtime/UnityEditorToolkit.Tests.asmdef | 23 + .../UnityEditorToolkit.Tests.asmdef.meta | 7 + skills/assets/unity-package/ThirdParty.meta | 8 + .../unity-package/ThirdParty/Npgsql.meta | 8 + .../unity-package/ThirdParty/Npgsql/.gitkeep | 0 .../assets/unity-package/ThirdParty/README.md | 189 +++ .../unity-package/ThirdParty/README.md.meta | 7 + .../unity-package/ThirdParty/UniTask.meta | 8 + .../unity-package/ThirdParty/UniTask/.gitkeep | 0 .../ThirdParty/websocket-sharp.meta | 8 + .../ThirdParty/websocket-sharp/README.md | 168 ++ .../ThirdParty/websocket-sharp/README.md.meta | 7 + .../ThirdParty/websocket-sharp/install.bat | 55 + .../websocket-sharp/install.bat.meta | 7 + .../ThirdParty/websocket-sharp/install.sh | 67 + .../websocket-sharp/install.sh.meta | 7 + .../websocket-sharp/websocket-sharp.dll | Bin 0 -> 250368 bytes .../websocket-sharp/websocket-sharp.dll.meta | 2 + skills/assets/unity-package/package.json | 39 + skills/assets/unity-package/package.json.meta | 7 + skills/references/COMMANDS.md | 324 ++++ skills/references/COMMANDS_ASSET.md | 549 +++++++ skills/references/COMMANDS_CHAIN.md | 329 ++++ skills/references/COMMANDS_COMPONENT.md | 629 ++++++++ .../references/COMMANDS_CONNECTION_STATUS.md | 99 ++ skills/references/COMMANDS_CONSOLE.md | 147 ++ skills/references/COMMANDS_EDITOR.md | 244 +++ .../COMMANDS_GAMEOBJECT_HIERARCHY.md | 383 +++++ skills/references/COMMANDS_MENU.md | 255 +++ skills/references/COMMANDS_PREFAB.md | 527 ++++++ skills/references/COMMANDS_PREFS.md | 250 +++ skills/references/COMMANDS_SCENE.md | 306 ++++ skills/references/COMMANDS_TRANSFORM.md | 215 +++ skills/references/COMMANDS_WAIT.md | 234 +++ skills/references/DATABASE_GUIDE.md | 693 ++++++++ skills/scripts/eslint.config.mjs | 43 + skills/scripts/package.json | 39 + skills/scripts/src/cli/cli.ts | 134 ++ skills/scripts/src/cli/commands/analytics.ts | 404 +++++ skills/scripts/src/cli/commands/animation.ts | 602 +++++++ skills/scripts/src/cli/commands/asset.ts | 695 ++++++++ skills/scripts/src/cli/commands/chain.ts | 263 +++ skills/scripts/src/cli/commands/component.ts | 743 +++++++++ skills/scripts/src/cli/commands/console.ts | 254 +++ skills/scripts/src/cli/commands/db.ts | 698 ++++++++ skills/scripts/src/cli/commands/editor.ts | 487 ++++++ skills/scripts/src/cli/commands/gameobject.ts | 285 ++++ skills/scripts/src/cli/commands/hierarchy.ts | 167 ++ skills/scripts/src/cli/commands/material.ts | 628 ++++++++ skills/scripts/src/cli/commands/menu.ts | 154 ++ skills/scripts/src/cli/commands/prefab.ts | 810 ++++++++++ skills/scripts/src/cli/commands/prefs.ts | 341 ++++ skills/scripts/src/cli/commands/scene.ts | 444 +++++ skills/scripts/src/cli/commands/snapshot.ts | 380 +++++ skills/scripts/src/cli/commands/sync.ts | 441 +++++ .../src/cli/commands/transform-history.ts | 383 +++++ skills/scripts/src/cli/commands/transform.ts | 436 +++++ skills/scripts/src/cli/commands/wait.ts | 209 +++ skills/scripts/src/constants/index.ts | 256 +++ skills/scripts/src/unity/client.ts | 343 ++++ skills/scripts/src/unity/protocol.ts | 205 +++ skills/scripts/src/utils/command-helpers.ts | 52 + skills/scripts/src/utils/config.ts | 152 ++ skills/scripts/src/utils/logger.ts | 144 ++ skills/scripts/src/utils/output-formatter.ts | 75 + skills/scripts/tsconfig.json | 25 + 254 files changed, 43797 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/SKILL.md create mode 100644 skills/assets/unity-package/Editor.meta create mode 100644 skills/assets/unity-package/Editor/Attributes.meta create mode 100644 skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs create mode 100644 skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database.meta create mode 100644 skills/assets/unity-package/Editor/Database/Commands.meta create mode 100644 skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs create mode 100644 skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs create mode 100644 skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs create mode 100644 skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs create mode 100644 skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs create mode 100644 skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/Commands/ICommand.cs create mode 100644 skills/assets/unity-package/Editor/Database/Commands/ICommand.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs create mode 100644 skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/DatabaseConfig.cs create mode 100644 skills/assets/unity-package/Editor/Database/DatabaseConfig.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/DatabaseManager.cs create mode 100644 skills/assets/unity-package/Editor/Database/DatabaseManager.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/MigrationRunner.cs create mode 100644 skills/assets/unity-package/Editor/Database/MigrationRunner.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/Migrations.meta create mode 100644 skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql create mode 100644 skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql.meta create mode 100644 skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql create mode 100644 skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql.meta create mode 100644 skills/assets/unity-package/Editor/Database/Models.meta create mode 100644 skills/assets/unity-package/Editor/Database/Models/.gitkeep create mode 100644 skills/assets/unity-package/Editor/Database/Queries.meta create mode 100644 skills/assets/unity-package/Editor/Database/Queries/.gitkeep create mode 100644 skills/assets/unity-package/Editor/Database/SQLiteConnector.cs create mode 100644 skills/assets/unity-package/Editor/Database/SQLiteConnector.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/Setup.meta create mode 100644 skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs create mode 100644 skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs create mode 100644 skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs.meta create mode 100644 skills/assets/unity-package/Editor/Database/SyncManager.cs create mode 100644 skills/assets/unity-package/Editor/Database/SyncManager.cs.meta create mode 100644 skills/assets/unity-package/Editor/DatabaseStatusWindow.cs create mode 100644 skills/assets/unity-package/Editor/DatabaseStatusWindow.cs.meta create mode 100644 skills/assets/unity-package/Editor/DatabaseStatusWindow.uxml create mode 100644 skills/assets/unity-package/Editor/DatabaseStatusWindow.uxml.meta create mode 100644 skills/assets/unity-package/Editor/EditorServerCLIInstaller.cs create mode 100644 skills/assets/unity-package/Editor/EditorServerCLIInstaller.cs.meta create mode 100644 skills/assets/unity-package/Editor/EditorServerCommandRunner.cs create mode 100644 skills/assets/unity-package/Editor/EditorServerCommandRunner.cs.meta create mode 100644 skills/assets/unity-package/Editor/EditorServerPathManager.cs create mode 100644 skills/assets/unity-package/Editor/EditorServerPathManager.cs.meta create mode 100644 skills/assets/unity-package/Editor/EditorServerWindow.cs create mode 100644 skills/assets/unity-package/Editor/EditorServerWindow.cs.meta create mode 100644 skills/assets/unity-package/Editor/EditorServerWindow.uss create mode 100644 skills/assets/unity-package/Editor/EditorServerWindow.uss.meta create mode 100644 skills/assets/unity-package/Editor/EditorServerWindow.uxml create mode 100644 skills/assets/unity-package/Editor/EditorServerWindow.uxml.meta create mode 100644 skills/assets/unity-package/Editor/EditorServerWindowData.cs create mode 100644 skills/assets/unity-package/Editor/EditorServerWindowData.cs.meta create mode 100644 skills/assets/unity-package/Editor/EditorServerWindowDatabase.cs create mode 100644 skills/assets/unity-package/Editor/EditorServerWindowDatabase.cs.meta create mode 100644 skills/assets/unity-package/Editor/EditorToolbarExtension.cs create mode 100644 skills/assets/unity-package/Editor/EditorToolbarExtension.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/AnalyticsHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/AnalyticsHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/AnimationHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/AnimationHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/AssetHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/AssetHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/BaseHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/BaseHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/ChainHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/ChainHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/ComponentHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/ComponentHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/ConsoleHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/ConsoleHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/DatabaseHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/DatabaseHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/EditorHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/EditorHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/GameObjectHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/GameObjectHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/HierarchyHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/HierarchyHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/MaterialHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/MaterialHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/MenuHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/MenuHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/PrefabHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/PrefabHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/PrefsHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/PrefsHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/SceneHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/SceneHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/SnapshotHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/SnapshotHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/SyncHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/SyncHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/TransformHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/TransformHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/TransformHistoryHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/TransformHistoryHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/Handlers/WaitHandler.cs create mode 100644 skills/assets/unity-package/Editor/Handlers/WaitHandler.cs.meta create mode 100644 skills/assets/unity-package/Editor/PackageInitializer.cs create mode 100644 skills/assets/unity-package/Editor/PackageInitializer.cs.meta create mode 100644 skills/assets/unity-package/Editor/Protocol.meta create mode 100644 skills/assets/unity-package/Editor/Protocol/JsonRpcError.cs create mode 100644 skills/assets/unity-package/Editor/Protocol/JsonRpcError.cs.meta create mode 100644 skills/assets/unity-package/Editor/Protocol/JsonRpcRequest.cs create mode 100644 skills/assets/unity-package/Editor/Protocol/JsonRpcRequest.cs.meta create mode 100644 skills/assets/unity-package/Editor/Protocol/JsonRpcResponse.cs create mode 100644 skills/assets/unity-package/Editor/Protocol/JsonRpcResponse.cs.meta create mode 100644 skills/assets/unity-package/Editor/Server.meta create mode 100644 skills/assets/unity-package/Editor/Server/EditorWebSocketServer.cs create mode 100644 skills/assets/unity-package/Editor/Server/EditorWebSocketServer.cs.meta create mode 100644 skills/assets/unity-package/Editor/Server/ServerStatus.cs create mode 100644 skills/assets/unity-package/Editor/Server/ServerStatus.cs.meta create mode 100644 skills/assets/unity-package/Editor/UnityEditorToolkit.Editor.asmdef create mode 100644 skills/assets/unity-package/Editor/UnityEditorToolkit.Editor.asmdef.meta create mode 100644 skills/assets/unity-package/Editor/Utils.meta create mode 100644 skills/assets/unity-package/Editor/Utils/EditorMainThreadDispatcher.cs create mode 100644 skills/assets/unity-package/Editor/Utils/EditorMainThreadDispatcher.cs.meta create mode 100644 skills/assets/unity-package/Editor/Utils/Logger.cs create mode 100644 skills/assets/unity-package/Editor/Utils/Logger.cs.meta create mode 100644 skills/assets/unity-package/Editor/Utils/ResponseQueue.cs create mode 100644 skills/assets/unity-package/Editor/Utils/ResponseQueue.cs.meta create mode 100644 skills/assets/unity-package/LICENSE.md create mode 100644 skills/assets/unity-package/LICENSE.md.meta create mode 100644 skills/assets/unity-package/README.ko.md create mode 100644 skills/assets/unity-package/README.ko.md.meta create mode 100644 skills/assets/unity-package/README.md create mode 100644 skills/assets/unity-package/README.md.meta create mode 100644 skills/assets/unity-package/Runtime.meta create mode 100644 skills/assets/unity-package/Runtime/GameObjectGuid.cs create mode 100644 skills/assets/unity-package/Runtime/GameObjectGuid.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Handlers.meta create mode 100644 skills/assets/unity-package/Runtime/Handlers/BaseHandler.cs create mode 100644 skills/assets/unity-package/Runtime/Handlers/BaseHandler.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Handlers/ConsoleHandler.cs create mode 100644 skills/assets/unity-package/Runtime/Handlers/ConsoleHandler.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Handlers/GameObjectHandler.cs create mode 100644 skills/assets/unity-package/Runtime/Handlers/GameObjectHandler.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Handlers/HierarchyHandler.cs create mode 100644 skills/assets/unity-package/Runtime/Handlers/HierarchyHandler.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Handlers/SceneHandler.cs create mode 100644 skills/assets/unity-package/Runtime/Handlers/SceneHandler.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Handlers/TransformHandler.cs create mode 100644 skills/assets/unity-package/Runtime/Handlers/TransformHandler.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Protocol.meta create mode 100644 skills/assets/unity-package/Runtime/Protocol/JsonRpcError.cs create mode 100644 skills/assets/unity-package/Runtime/Protocol/JsonRpcError.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Protocol/JsonRpcRequest.cs create mode 100644 skills/assets/unity-package/Runtime/Protocol/JsonRpcRequest.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Protocol/JsonRpcResponse.cs create mode 100644 skills/assets/unity-package/Runtime/Protocol/JsonRpcResponse.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Server.meta create mode 100644 skills/assets/unity-package/Runtime/Server/ServerStatus.cs create mode 100644 skills/assets/unity-package/Runtime/Server/ServerStatus.cs.meta create mode 100644 skills/assets/unity-package/Runtime/Server/UnityEditorServer.cs create mode 100644 skills/assets/unity-package/Runtime/Server/UnityEditorServer.cs.meta create mode 100644 skills/assets/unity-package/Runtime/UnityEditorToolkit.asmdef create mode 100644 skills/assets/unity-package/Runtime/UnityEditorToolkit.asmdef.meta create mode 100644 skills/assets/unity-package/Runtime/Utils.meta create mode 100644 skills/assets/unity-package/Runtime/Utils/UnityMainThreadDispatcher.cs create mode 100644 skills/assets/unity-package/Runtime/Utils/UnityMainThreadDispatcher.cs.meta create mode 100644 skills/assets/unity-package/Tests.meta create mode 100644 skills/assets/unity-package/Tests/Editor.meta create mode 100644 skills/assets/unity-package/Tests/Editor/GameObjectCachingTests.cs create mode 100644 skills/assets/unity-package/Tests/Editor/GameObjectCachingTests.cs.meta create mode 100644 skills/assets/unity-package/Tests/Editor/JsonRpcProtocolTests.cs create mode 100644 skills/assets/unity-package/Tests/Editor/JsonRpcProtocolTests.cs.meta create mode 100644 skills/assets/unity-package/Tests/Editor/UnityEditorToolkit.Editor.Tests.asmdef create mode 100644 skills/assets/unity-package/Tests/Editor/UnityEditorToolkit.Editor.Tests.asmdef.meta create mode 100644 skills/assets/unity-package/Tests/Editor/UnityMainThreadDispatcherTests.cs create mode 100644 skills/assets/unity-package/Tests/Editor/UnityMainThreadDispatcherTests.cs.meta create mode 100644 skills/assets/unity-package/Tests/Editor/Vector3ValidationTests.cs create mode 100644 skills/assets/unity-package/Tests/Editor/Vector3ValidationTests.cs.meta create mode 100644 skills/assets/unity-package/Tests/Runtime.meta create mode 100644 skills/assets/unity-package/Tests/Runtime/UnityEditorToolkit.Tests.asmdef create mode 100644 skills/assets/unity-package/Tests/Runtime/UnityEditorToolkit.Tests.asmdef.meta create mode 100644 skills/assets/unity-package/ThirdParty.meta create mode 100644 skills/assets/unity-package/ThirdParty/Npgsql.meta create mode 100644 skills/assets/unity-package/ThirdParty/Npgsql/.gitkeep create mode 100644 skills/assets/unity-package/ThirdParty/README.md create mode 100644 skills/assets/unity-package/ThirdParty/README.md.meta create mode 100644 skills/assets/unity-package/ThirdParty/UniTask.meta create mode 100644 skills/assets/unity-package/ThirdParty/UniTask/.gitkeep create mode 100644 skills/assets/unity-package/ThirdParty/websocket-sharp.meta create mode 100644 skills/assets/unity-package/ThirdParty/websocket-sharp/README.md create mode 100644 skills/assets/unity-package/ThirdParty/websocket-sharp/README.md.meta create mode 100644 skills/assets/unity-package/ThirdParty/websocket-sharp/install.bat create mode 100644 skills/assets/unity-package/ThirdParty/websocket-sharp/install.bat.meta create mode 100644 skills/assets/unity-package/ThirdParty/websocket-sharp/install.sh create mode 100644 skills/assets/unity-package/ThirdParty/websocket-sharp/install.sh.meta create mode 100644 skills/assets/unity-package/ThirdParty/websocket-sharp/websocket-sharp.dll create mode 100644 skills/assets/unity-package/ThirdParty/websocket-sharp/websocket-sharp.dll.meta create mode 100644 skills/assets/unity-package/package.json create mode 100644 skills/assets/unity-package/package.json.meta create mode 100644 skills/references/COMMANDS.md create mode 100644 skills/references/COMMANDS_ASSET.md create mode 100644 skills/references/COMMANDS_CHAIN.md create mode 100644 skills/references/COMMANDS_COMPONENT.md create mode 100644 skills/references/COMMANDS_CONNECTION_STATUS.md create mode 100644 skills/references/COMMANDS_CONSOLE.md create mode 100644 skills/references/COMMANDS_EDITOR.md create mode 100644 skills/references/COMMANDS_GAMEOBJECT_HIERARCHY.md create mode 100644 skills/references/COMMANDS_MENU.md create mode 100644 skills/references/COMMANDS_PREFAB.md create mode 100644 skills/references/COMMANDS_PREFS.md create mode 100644 skills/references/COMMANDS_SCENE.md create mode 100644 skills/references/COMMANDS_TRANSFORM.md create mode 100644 skills/references/COMMANDS_WAIT.md create mode 100644 skills/references/DATABASE_GUIDE.md create mode 100644 skills/scripts/eslint.config.mjs create mode 100644 skills/scripts/package.json create mode 100644 skills/scripts/src/cli/cli.ts create mode 100644 skills/scripts/src/cli/commands/analytics.ts create mode 100644 skills/scripts/src/cli/commands/animation.ts create mode 100644 skills/scripts/src/cli/commands/asset.ts create mode 100644 skills/scripts/src/cli/commands/chain.ts create mode 100644 skills/scripts/src/cli/commands/component.ts create mode 100644 skills/scripts/src/cli/commands/console.ts create mode 100644 skills/scripts/src/cli/commands/db.ts create mode 100644 skills/scripts/src/cli/commands/editor.ts create mode 100644 skills/scripts/src/cli/commands/gameobject.ts create mode 100644 skills/scripts/src/cli/commands/hierarchy.ts create mode 100644 skills/scripts/src/cli/commands/material.ts create mode 100644 skills/scripts/src/cli/commands/menu.ts create mode 100644 skills/scripts/src/cli/commands/prefab.ts create mode 100644 skills/scripts/src/cli/commands/prefs.ts create mode 100644 skills/scripts/src/cli/commands/scene.ts create mode 100644 skills/scripts/src/cli/commands/snapshot.ts create mode 100644 skills/scripts/src/cli/commands/sync.ts create mode 100644 skills/scripts/src/cli/commands/transform-history.ts create mode 100644 skills/scripts/src/cli/commands/transform.ts create mode 100644 skills/scripts/src/cli/commands/wait.ts create mode 100644 skills/scripts/src/constants/index.ts create mode 100644 skills/scripts/src/unity/client.ts create mode 100644 skills/scripts/src/unity/protocol.ts create mode 100644 skills/scripts/src/utils/command-helpers.ts create mode 100644 skills/scripts/src/utils/config.ts create mode 100644 skills/scripts/src/utils/logger.ts create mode 100644 skills/scripts/src/utils/output-formatter.ts create mode 100644 skills/scripts/tsconfig.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..932b1be --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "unity-editor-toolkit", + "description": "Complete Unity Editor control toolkit with real-time WebSocket communication, SQLite database integration, and GUID-based persistence for GameObjects, hierarchy, scenes, console, and more", + "version": "0.12.1", + "author": { + "name": "Dev GOM", + "url": "https://github.com/Dev-GOM/claude-code-marketplace" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..40b7d7e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# unity-editor-toolkit + +Complete Unity Editor control toolkit with real-time WebSocket communication, SQLite database integration, and GUID-based persistence for GameObjects, hierarchy, scenes, console, and more diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..3cae65d --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,1045 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:Dev-GOM/claude-code-marketplace:plugins/unity-editor-toolkit", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "addec58b61736ee1688dec62f7eb6871161d8315", + "treeHash": "68ef93451a78795c55a9f6b79a9d9f69f9559382bd97dde8918f3a36627a8478", + "generatedAt": "2025-11-28T10:10:18.396026Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "unity-editor-toolkit", + "description": "Complete Unity Editor control toolkit with real-time WebSocket communication, SQLite database integration, and GUID-based persistence for GameObjects, hierarchy, scenes, console, and more", + "version": "0.12.1" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "e3c79b46cc3ed01b6be81d061d2a0b9794d9641fd87efc73f63f43e97bac695f" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "53a76c059cc09942d93f0c377141b61a2592a69a52be8e3d5adbb8290fdcc0fc" + }, + { + "path": "skills/SKILL.md", + "sha256": "f5035223bbca7351c7656183af32a262aad696654e0e94d6af76cf01df940ce3" + }, + { + "path": "skills/references/COMMANDS_EDITOR.md", + "sha256": "e2585b9948b3ecfec08f3b10cb25089df6011e4720a7aca9a1ede1b34f998af6" + }, + { + "path": "skills/references/COMMANDS_PREFS.md", + "sha256": "3eecd5dc634b32849740094a592107955c7f85cadc06cc491bea7ed7aaebb00e" + }, + { + "path": "skills/references/COMMANDS_MENU.md", + "sha256": "85017778b13111e6e3fa3de752d9bfb5dff0dd738932164534971769dc8709ad" + }, + { + "path": "skills/references/DATABASE_GUIDE.md", + "sha256": "fbe09d5fd3d053901e41d16d12ba2c09fb572f185a5602b78aae0c1c2aa7d8d6" + }, + { + "path": "skills/references/COMMANDS.md", + "sha256": "58617287321590828db19a51939c3025bddf35866e18fae22fa5a09edbf49539" + }, + { + "path": "skills/references/COMMANDS_COMPONENT.md", + "sha256": "81f7e0077041d95175dde2821e51dabc32ebf94d2cdb4a6cf95d0dce2f19d17c" + }, + { + "path": "skills/references/COMMANDS_ASSET.md", + "sha256": "ea30a731d171b13e3ad8960317894c0ba8b8a0d38955299db35bc55fb6cd2809" + }, + { + "path": "skills/references/COMMANDS_CHAIN.md", + "sha256": "c6f40c93ea295010f5ba27ffff2cb6d147a0b7eaa1d967eec9d6eb85cbca5204" + }, + { + "path": "skills/references/COMMANDS_PREFAB.md", + "sha256": "20c7ebadc6b39f71af2c7677e8e8f15d94acab340c759e841f893f7e5c94b9b2" + }, + { + "path": "skills/references/COMMANDS_TRANSFORM.md", + "sha256": "cb94e66d55093809ca0c2b71bf3e5cf4a9b3d8056e7c2c339a8e46bb5d57f2ea" + }, + { + "path": "skills/references/COMMANDS_CONNECTION_STATUS.md", + "sha256": "7c00844269981310fcdb3b61b7a5f9839411b8800efe2951345dc4d8ffb4a384" + }, + { + "path": "skills/references/COMMANDS_CONSOLE.md", + "sha256": "80b17b3798a9a664608f72edecdaaa457c70f21f1ec904531720c30015ede254" + }, + { + "path": "skills/references/COMMANDS_SCENE.md", + "sha256": "b74a0a0d525270fc891de4d8b3d31fbfea65387b18b5dee6a73628b21fc3f9f1" + }, + { + "path": "skills/references/COMMANDS_GAMEOBJECT_HIERARCHY.md", + "sha256": "75c17c3a07c047f79df2933619764a1753d4682ea09fe5d74d22ecdac9a537ad" + }, + { + "path": "skills/references/COMMANDS_WAIT.md", + "sha256": "17431e1d0281c834655a2eef3ca661c3a05400981516c739d40c3a2be46ae848" + }, + { + "path": "skills/scripts/package.json", + "sha256": "2357f10d5349fa6deb5fba40abc701dc0508956a96b33a7b17323387383e06a0" + }, + { + "path": "skills/scripts/tsconfig.json", + "sha256": "857b9a0ce2e6cccd6578be99355fbff46899f0163af04f15ef62ccd6dff94f74" + }, + { + "path": "skills/scripts/eslint.config.mjs", + "sha256": "1147567cc49454b578398e51774ef908ac51bc069611bd9aaf66ed6b9293818f" + }, + { + "path": "skills/scripts/src/unity/client.ts", + "sha256": "1d69ee2fe81a1cda91437effca1de14f8deba0b4ae796693dad0d5a97f015185" + }, + { + "path": "skills/scripts/src/unity/protocol.ts", + "sha256": "11f486aae9a97f6aa58bd59397dafd2edd45452ce6a234db07b351933b087e6e" + }, + { + "path": "skills/scripts/src/constants/index.ts", + "sha256": "c8cdadbe22d8ad4f8551922e7130f413f2aec636722d2ca00476cb72399ace97" + }, + { + "path": "skills/scripts/src/utils/command-helpers.ts", + "sha256": "dfb42d99cfea3c7a1b0378ecaf0ca1b254bbbfbb333d9aabb1d0f1abb45c9a62" + }, + { + "path": "skills/scripts/src/utils/output-formatter.ts", + "sha256": "e73d4b046f35976ba714e1d3655041abe4f7ffeb4d4b39e99eddcd399062e029" + }, + { + "path": "skills/scripts/src/utils/logger.ts", + "sha256": "81ad61fa1bae16a73f0593bc7e7f0bda5b9425de035082e81e770026bfa46fbc" + }, + { + "path": "skills/scripts/src/utils/config.ts", + "sha256": "d38f85e7ea1ff1a2c8c87f021ff111e3b1d4fd81fa744706fb9f571f6ba29feb" + }, + { + "path": "skills/scripts/src/cli/cli.ts", + "sha256": "906cba4187d8e5bc31cf715f31aaa4c68c3f8bf93de643890025b1f8001c4a83" + }, + { + "path": "skills/scripts/src/cli/commands/snapshot.ts", + "sha256": "39bfa5e8fb0fc27e8bc52e471a5572f50d6ab0194e51275ee9bd8f69d02893e4" + }, + { + "path": "skills/scripts/src/cli/commands/component.ts", + "sha256": "bbe1d1b49ecd8f82add1f0692292bc86b89d08c2c6dfbb002e797aa8ce573ea2" + }, + { + "path": "skills/scripts/src/cli/commands/transform-history.ts", + "sha256": "63f1b95869b65850a95f6623f3fc086175458cc46c18c9ec10ec933d6572b9c0" + }, + { + "path": "skills/scripts/src/cli/commands/editor.ts", + "sha256": "ed59e3d1ab1eecaf6f4fd285ed5a6b69acdcc73a67d58c6b2b8cdb5a7edabfb0" + }, + { + "path": "skills/scripts/src/cli/commands/analytics.ts", + "sha256": "5a20825572d9f19f0b947cdab84c2f5a13bb18cda50fc78756cee6b049051935" + }, + { + "path": "skills/scripts/src/cli/commands/transform.ts", + "sha256": "64206f9fe58a0fb103469905503df06a8fac0ec60bb321cb903b40621665d6ac" + }, + { + "path": "skills/scripts/src/cli/commands/gameobject.ts", + "sha256": "03cc715a551aa59fe51e3e06b406f001f845a0d487b448902e22918cbb4cb7bd" + }, + { + "path": "skills/scripts/src/cli/commands/menu.ts", + "sha256": "9321aa86ddb9981585d4a38006a313ee67a1376aad066b1135893683e1959c9f" + }, + { + "path": "skills/scripts/src/cli/commands/prefs.ts", + "sha256": "d4415d9cdce9191326d5f9b834efb49e46a36101912f671205fa725e1a3a1510" + }, + { + "path": "skills/scripts/src/cli/commands/animation.ts", + "sha256": "e7845007d5950f5cda339a6b40e66a5f6afd0a37131159002bb3775254079f1d" + }, + { + "path": "skills/scripts/src/cli/commands/hierarchy.ts", + "sha256": "bff78218713128caf2d9614f41197a89b9012ca6a277d5e08c6aef17a30e32ae" + }, + { + "path": "skills/scripts/src/cli/commands/scene.ts", + "sha256": "9e0b83c912175f228d4e49baa2f5c88eebca522166560a16d05b2a8f96fc2608" + }, + { + "path": "skills/scripts/src/cli/commands/wait.ts", + "sha256": "d37984c32a3e1df97174bc2e34a0b4e7155b47f63ec44358772b319f56a7799b" + }, + { + "path": "skills/scripts/src/cli/commands/chain.ts", + "sha256": "d5107555dfcff127db7ab2aa762305225ccae12ff4602e159c93c21fff4e7b04" + }, + { + "path": "skills/scripts/src/cli/commands/asset.ts", + "sha256": "68800f1c7b6599b5a2d16dc667541c77466aed51f2b89b23d8af0cad848ea94a" + }, + { + "path": "skills/scripts/src/cli/commands/prefab.ts", + "sha256": "7078d912cffc685055e643482721736f67c11389296e795087ea31fb2d194f83" + }, + { + "path": "skills/scripts/src/cli/commands/material.ts", + "sha256": "817543bb289455f27f2d9eeb90fd68f9df7e25399ea0cc3448e66b8e14daf7f9" + }, + { + "path": "skills/scripts/src/cli/commands/db.ts", + "sha256": "650c17d32eac152ecca0ab61d071a0569a5dca48b7341091c5eeb92324df8933" + }, + { + "path": "skills/scripts/src/cli/commands/console.ts", + "sha256": "42fca399c34d64ea47a48edc524004dde99ec5d8962606f1c2aac9977d93d5d7" + }, + { + "path": "skills/scripts/src/cli/commands/sync.ts", + "sha256": "7eb8a5a9fb95f51916036b264db77de08146b73530f8736ecfa47af63e87e3ca" + }, + { + "path": "skills/assets/unity-package/LICENSE.md", + "sha256": "6fe6555bd8d7a34d5f217149f3a0a3768fb22b28c9339dc41bf0d6bc5b0f5ea1" + }, + { + "path": "skills/assets/unity-package/package.json.meta", + "sha256": "13c74e45c8ff89bfddb663c5f764eccae8f811819939b81adfb7cbf9ec2e117a" + }, + { + "path": "skills/assets/unity-package/Editor.meta", + "sha256": "0da46452b34dbaecee0bce5e555daec8f330df5dcba44659e0098a5e9ca88174" + }, + { + "path": "skills/assets/unity-package/README.md", + "sha256": "57971de30c840782e287a09f31d903ebea6e4235e54e73c4ebc584bb2431d4c4" + }, + { + "path": "skills/assets/unity-package/LICENSE.md.meta", + "sha256": "ed0b4e2b9118f58779d3baea013bf5a809b85a2a4a9c30f2bc79debba0f0c1c0" + }, + { + "path": "skills/assets/unity-package/README.ko.md.meta", + "sha256": "9eb13d1babe386c0b307515eaf44a39263699dedcd173974dbd30a6498dadd7a" + }, + { + "path": "skills/assets/unity-package/package.json", + "sha256": "a7a876d27ebf3224595130dffd04ae2a16981880bde35faae7b18134bcad8f59" + }, + { + "path": "skills/assets/unity-package/ThirdParty.meta", + "sha256": "d64f8677aa82c5c83af3fe84b6703064989a13fddd1c309fbc7bad5f27e53f60" + }, + { + "path": "skills/assets/unity-package/README.ko.md", + "sha256": "5dc0499ede29444adc605169aef0f479175a129f16a73814b57e34a3fa02306b" + }, + { + "path": "skills/assets/unity-package/Tests.meta", + "sha256": "f0f363ab9b4546495e886a5896ef09dc8a30db7647a1bb237c0e1d6e9d1e2630" + }, + { + "path": "skills/assets/unity-package/README.md.meta", + "sha256": "3550600b18de92cfd4f51c76be9507aa634b4b4db368fa9019d29bb6e3059e1a" + }, + { + "path": "skills/assets/unity-package/Runtime.meta", + "sha256": "33242fcc16e3a874fb5cc4ffab42647cdeddd70c898e02df648dab1bd2703e2d" + }, + { + "path": "skills/assets/unity-package/Runtime/Protocol.meta", + "sha256": "3c1c5503b09188fce161042dc7dce2f8cc28e5ee7e11bd3330bca940f48fd797" + }, + { + "path": "skills/assets/unity-package/Runtime/GameObjectGuid.cs.meta", + "sha256": "2466611b0a5fb45812109500c548d86c9dfff9a0b95971a7bed4612fe6cc834b" + }, + { + "path": "skills/assets/unity-package/Runtime/Utils.meta", + "sha256": "c72926d2c5494d4c1f552bfde30f5c6aeb261cd6a7abb751a5f5017c04d69a54" + }, + { + "path": "skills/assets/unity-package/Runtime/UnityEditorToolkit.asmdef.meta", + "sha256": "9b8214e8a43af598f6e4ed1ae454d7fd47278f0735291e6e13a03a30349ea179" + }, + { + "path": "skills/assets/unity-package/Runtime/UnityEditorToolkit.asmdef", + "sha256": "ed129b5b5842fb387aeeb35374a716ba50c1ed726d494ed62e871467956b2e3d" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers.meta", + "sha256": "2af11f6483ef3ae3a4d34c5cf327137809af7592c73088f1307042f190970940" + }, + { + "path": "skills/assets/unity-package/Runtime/GameObjectGuid.cs", + "sha256": "6a6b29b65ff72dbd88b816466cd77faed01bb2ef80f4a52025c9d01a10c86150" + }, + { + "path": "skills/assets/unity-package/Runtime/Server.meta", + "sha256": "aecde2f86aeb70ab4e6a2f49c0a7269d454cba5393737b8d22501737faa655ae" + }, + { + "path": "skills/assets/unity-package/Runtime/Server/ServerStatus.cs.meta", + "sha256": "38c6757593b7f39b70eb34c74c32399376e2d7f11827a8a6918cfd10b9509443" + }, + { + "path": "skills/assets/unity-package/Runtime/Server/UnityEditorServer.cs", + "sha256": "3f60450c9ac7209ad990e4ab5441c78e6fa0685efdd5151dbb13f0434f4f4909" + }, + { + "path": "skills/assets/unity-package/Runtime/Server/UnityEditorServer.cs.meta", + "sha256": "06af29e6cebd57d033f17d99ad962353d86f247351f0f010310df003c79baaf9" + }, + { + "path": "skills/assets/unity-package/Runtime/Server/ServerStatus.cs", + "sha256": "2f8c8d1061760293222d3fb6182e51245fbfebf436e710196842b2dae74f3722" + }, + { + "path": "skills/assets/unity-package/Runtime/Utils/UnityMainThreadDispatcher.cs", + "sha256": "522ed9c45128d15257d7708f5633c62a03fceb7b52c00bb6f448aa7be58fefc4" + }, + { + "path": "skills/assets/unity-package/Runtime/Utils/UnityMainThreadDispatcher.cs.meta", + "sha256": "49b8c1d2a34a92bc9fca6da853c053a1886264fa8bdcce29490535840b01cc8c" + }, + { + "path": "skills/assets/unity-package/Runtime/Protocol/JsonRpcResponse.cs", + "sha256": "0be79db464d8615e7e617943b759556a0f8ad0596cf53fecb8339108b1b241a0" + }, + { + "path": "skills/assets/unity-package/Runtime/Protocol/JsonRpcRequest.cs", + "sha256": "eb2f29bcce26e0351dc9a6402c4db111c7b14c4bd7890c195401945f7cc0568d" + }, + { + "path": "skills/assets/unity-package/Runtime/Protocol/JsonRpcResponse.cs.meta", + "sha256": "952e5e853d13bb1c1ca0fabafb4935f047789fe438dd143b9859cdf4e5019b05" + }, + { + "path": "skills/assets/unity-package/Runtime/Protocol/JsonRpcError.cs.meta", + "sha256": "62648768bffb7e2084c27291f6c776c4e1d3e67b1ef6dd5418221d15c25babee" + }, + { + "path": "skills/assets/unity-package/Runtime/Protocol/JsonRpcRequest.cs.meta", + "sha256": "e31ad43ebd0708a1df1c3cbbb4090ad8785fc8e034afe81a32fb85ee59de3e68" + }, + { + "path": "skills/assets/unity-package/Runtime/Protocol/JsonRpcError.cs", + "sha256": "5e02d1af93d913cac7d39ce499c6198ccb15f83b1bcd1a65240965ded1b8281c" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/HierarchyHandler.cs.meta", + "sha256": "3e0d8b31040716e2ddc6e88f18d96774a52221d3048bc4d0a86abe3ae0fa550b" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/ConsoleHandler.cs", + "sha256": "4e0054e3c944fb0727444563d6a5d6d56fe6ff273aa71c6f29739e6239f3d4ab" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/TransformHandler.cs.meta", + "sha256": "7a9bc9bee50a01a19865958dae4b4a51490f8a78bdd926567262d1db187c96fc" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/SceneHandler.cs.meta", + "sha256": "04d375520f66687db07a332861b54db704f2522a95f2e009461275ab7591e6ad" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/HierarchyHandler.cs", + "sha256": "176027193d8083df500b7dad1e51c2e0ded369f924fd461fa75dd6727ea3678d" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/BaseHandler.cs.meta", + "sha256": "463dcc5c053e6dd4af9dc7fa2d97ce285c9b8b24e9056fc95e4b931299fe5b95" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/GameObjectHandler.cs.meta", + "sha256": "7127341e2551531e879b7fa3570122e6fff9995430fc912c5e369ca82dcafd1b" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/SceneHandler.cs", + "sha256": "6d95b5fcd6cdc5c7fdc779db1fb7a686d3f1f8869594cd3967d04ddc19ef9d7e" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/BaseHandler.cs", + "sha256": "20a350311382e17fa983ef85d84842134fb34df217621b4ff433f2c9f35aedaa" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/TransformHandler.cs", + "sha256": "11b387b667fba6e5370586899adb3df09427b701175af500ccf748df9425b0a5" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/ConsoleHandler.cs.meta", + "sha256": "1c5cf970189614965ac41074059c4a6ee0b13432179dbbe0354c4e8cb1dab6ef" + }, + { + "path": "skills/assets/unity-package/Runtime/Handlers/GameObjectHandler.cs", + "sha256": "d6225f4d33e8e398b68db83945a732a4c7984a587ac607cc21e1b64e5d5c5b87" + }, + { + "path": "skills/assets/unity-package/Tests/Editor.meta", + "sha256": "028fd366066e2f95036c8039645ba9d11f091af995b789e8da6d4a57deb458bd" + }, + { + "path": "skills/assets/unity-package/Tests/Runtime.meta", + "sha256": "5a50cc07b6bdf8e460b4d7f13614068de3453d4e903237b242099d7da76ef485" + }, + { + "path": "skills/assets/unity-package/Tests/Runtime/UnityEditorToolkit.Tests.asmdef", + "sha256": "e5b0b59463cdb21c75cfbba72b5650924e7e98c4eabf77ef56e227471846cb0b" + }, + { + "path": "skills/assets/unity-package/Tests/Runtime/UnityEditorToolkit.Tests.asmdef.meta", + "sha256": "850ff587fd0c8c9601471e7b2fc3020a20127ccdb6edb0cbd34bd8acc7b54718" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/UnityMainThreadDispatcherTests.cs", + "sha256": "9d28e13ed2e2a2d8366b58d8b6c49ea3c3382c7a6bee2a67b63f5584ee7fe383" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/Vector3ValidationTests.cs", + "sha256": "0c3570fc1f00ffd4d45be5207c42aa72714e8270135f37b814939d4846d4315e" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/GameObjectCachingTests.cs", + "sha256": "5f8a1c035e949152eaef0ee8ffce674492cc5d3725659d8060c52087cfa3387b" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/GameObjectCachingTests.cs.meta", + "sha256": "43b9c7f5545e71d72b950aff82d430470016d179e18f1c0756e3f18c04847426" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/Vector3ValidationTests.cs.meta", + "sha256": "18787e249eb05f5a6ddebfc18caf351dbf8e401381a7244026374338d9debd4e" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/JsonRpcProtocolTests.cs", + "sha256": "0ed52b76848cc92eeb9789cc96d1360067c19d16fea948dc655d271e9bc5be82" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/JsonRpcProtocolTests.cs.meta", + "sha256": "ba688eab65ffcc39a4956f09329659f425dd3dc9513ad6c1828ed9ccc791fc2a" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/UnityEditorToolkit.Editor.Tests.asmdef", + "sha256": "bc35e3211c7aea33dc204c9dd4a9ccfd9f1b9b407d607feb1ee71f2078e33b4a" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/UnityMainThreadDispatcherTests.cs.meta", + "sha256": "caf3c420ef9dc349f0b9848fa8d6e5e70e6b9d13d2b4b4a6f3704d458af33336" + }, + { + "path": "skills/assets/unity-package/Tests/Editor/UnityEditorToolkit.Editor.Tests.asmdef.meta", + "sha256": "d992b1693828f41f5740cd250aa016b0ab7912faeac54d00130810e499113ec6" + }, + { + "path": "skills/assets/unity-package/ThirdParty/Npgsql.meta", + "sha256": "e278bfbab760ee27889778d280af6e835e4aeaf693f2bdcefc8835c427aeef78" + }, + { + "path": "skills/assets/unity-package/ThirdParty/README.md", + "sha256": "aff7b49c6276c3445dba277c25ca1ce7cf58c1dc2a8b1f526546e23116c452c7" + }, + { + "path": "skills/assets/unity-package/ThirdParty/README.md.meta", + "sha256": "858287feafbcc9ac1ae903ad83a5a67c8fd4e42917a60ca29d428500da70172f" + }, + { + "path": "skills/assets/unity-package/ThirdParty/websocket-sharp.meta", + "sha256": "0d7e2dbdfe9571ca7f589eabe67e244643082cb05bfb7eb54362f9bc80525896" + }, + { + "path": "skills/assets/unity-package/ThirdParty/UniTask.meta", + "sha256": "e1402a917ff8180076e80da6a367c8e6ee36f9c0377ae494ed94afff1cece684" + }, + { + "path": "skills/assets/unity-package/ThirdParty/websocket-sharp/websocket-sharp.dll", + "sha256": "fb06ffceb4f8789c893d2f292e5810927dd7266d3bad68df2cedb8775500e8be" + }, + { + "path": "skills/assets/unity-package/ThirdParty/websocket-sharp/websocket-sharp.dll.meta", + "sha256": "5b24d9a316efe7bfd7d0682f58a87952a5f1c31e582f333daa557a972e55a4b9" + }, + { + "path": "skills/assets/unity-package/ThirdParty/websocket-sharp/install.sh", + "sha256": "c9baa53a91420c2c530d3deb67f5178938ce239044ae3d879b1daaa71809ed3b" + }, + { + "path": "skills/assets/unity-package/ThirdParty/websocket-sharp/README.md", + "sha256": "4fb5974f28fb030298e606a88f394a57f3111b4eb0ce419708e6ba955b1a4847" + }, + { + "path": "skills/assets/unity-package/ThirdParty/websocket-sharp/install.bat", + "sha256": "5f27dd5430ee3759fea7f30e4b46ca39fa89c3aaff9dcf046696c567077fb3fa" + }, + { + "path": "skills/assets/unity-package/ThirdParty/websocket-sharp/install.sh.meta", + "sha256": "bea19d52b6f4ef6f03232ac2d323cad7e0b0bbee4459ecae313830d07e45e295" + }, + { + "path": "skills/assets/unity-package/ThirdParty/websocket-sharp/install.bat.meta", + "sha256": "3f83ae5d2e70e20009631b3a710575ea613373d5e2880d21ed35946bdb87b448" + }, + { + "path": "skills/assets/unity-package/ThirdParty/websocket-sharp/README.md.meta", + "sha256": "cab920d952315bd05dc31daed466c65a86a8d038b2714b92a4e2881ccdcc1870" + }, + { + "path": "skills/assets/unity-package/ThirdParty/Npgsql/.gitkeep", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "skills/assets/unity-package/ThirdParty/UniTask/.gitkeep", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerCommandRunner.cs.meta", + "sha256": "9796c73e2131596fbea40a34ca77286391670452b983356dca3184978c0d09d9" + }, + { + "path": "skills/assets/unity-package/Editor/Attributes.meta", + "sha256": "daf1406d74d8bac125698a36df8790035578c7df4a8da0841408d7f32b31a969" + }, + { + "path": "skills/assets/unity-package/Editor/EditorToolbarExtension.cs.meta", + "sha256": "cd27a96c5f1efd719e091fb91b5afd20b5688ec6f8b4155e6655ea2f613bf134" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindow.uss", + "sha256": "3f78860b7114129bb8f6722cdb1eaab5272e7a9156e2ab9667b6f220a719d6dd" + }, + { + "path": "skills/assets/unity-package/Editor/DatabaseStatusWindow.uxml", + "sha256": "75f812952c888cebe991a2ee97b852c94197ce81f4bf1d27906b730f29b436a9" + }, + { + "path": "skills/assets/unity-package/Editor/PackageInitializer.cs", + "sha256": "d30a8e99f8ebccbd439d7992cc448ff225759285d1a3954fb0eb244fe015b460" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerCLIInstaller.cs", + "sha256": "cf46d73fa41a5031c750e7a3aca0b8ed9049024df0c345f0a53b21cda5462926" + }, + { + "path": "skills/assets/unity-package/Editor/Protocol.meta", + "sha256": "87c307598a84eef1057dcea57a3701ff4b91c859404b23b8bf03fac1ed652765" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindow.uxml", + "sha256": "08f89a0fab85782d5d43691bfe5b97a449d774dbcbb7801d53c8f9bd42b86648" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindowData.cs.meta", + "sha256": "ea637d28e5858da2e2cb8bbc0f6af177fae0cc76794bf4785c537c4b28232cc7" + }, + { + "path": "skills/assets/unity-package/Editor/Database.meta", + "sha256": "54c9d4efae47dc6bc843c059b11c1253108c89d4d1f7d7213c96856a00df269e" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerCommandRunner.cs", + "sha256": "48c1255e3ce0bb0f9fa7092cc3c06319a67b6621a64d6f5a7240a4c419fa6340" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindow.cs.meta", + "sha256": "2f5df0726f9c010424102a85c3de9dee9bf5b8a6468b54844bbe2f65f8592fd6" + }, + { + "path": "skills/assets/unity-package/Editor/PackageInitializer.cs.meta", + "sha256": "f47fa0b1e1cf8d4c7320efaa7d8448ec7219b888600f886d8e51cc549078c2d7" + }, + { + "path": "skills/assets/unity-package/Editor/Utils.meta", + "sha256": "5ed0ff9829bca14d5033d3ac6c8d01a4ab2d136b8fc738f1cbaf89ab830072c6" + }, + { + "path": "skills/assets/unity-package/Editor/DatabaseStatusWindow.uxml.meta", + "sha256": "4e8726617f3d1e91cbf2641e426bda0ae9256387af0c2133d7696ba5496f6b03" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindowDatabase.cs", + "sha256": "a5723bd6d08635132d6f273fdaa78070f5ac5fd141cc01e3d55ba798cf3c2cb3" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers.meta", + "sha256": "3f1908944f55ab421004a5fbef54836b25853d3cab9c1550045ff9189bea59c0" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindow.uxml.meta", + "sha256": "a13e292ab7f336f82e2083445638f5f41a650678cf43d972aaff7b44f4782f69" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerPathManager.cs", + "sha256": "132c02d6327499792729bb8edce05ebf3b9f8f69780df393ddb79756b5aafe55" + }, + { + "path": "skills/assets/unity-package/Editor/UnityEditorToolkit.Editor.asmdef.meta", + "sha256": "5607085894a21d7a354320630f9df5b9832e60f56ff5d78f9ca66ff1275e5317" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerCLIInstaller.cs.meta", + "sha256": "a55b36ce9a600192395b2d2f87f29ce87cc4d6c9f2cca48349880fc14fa5425a" + }, + { + "path": "skills/assets/unity-package/Editor/DatabaseStatusWindow.cs", + "sha256": "4de5d8f4a52d715e21d0f6185a7769352d0cedbccabe84c7951c3e45b10fda97" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerPathManager.cs.meta", + "sha256": "219aa08acea43987beda9e3885d87ee99194f8218d5b485e78ebf18a6f5042b0" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindow.uss.meta", + "sha256": "ddd4085190ecdb8690e4eb910d0dcf51820bab247794cfec6a8ad2ae04a94627" + }, + { + "path": "skills/assets/unity-package/Editor/DatabaseStatusWindow.cs.meta", + "sha256": "0c1f602d732ce0eb8213143a9f4c8315969106f2157928b5ffb7e0d8da1cdc5f" + }, + { + "path": "skills/assets/unity-package/Editor/Server.meta", + "sha256": "4a615ce22ae99a0186769e4d1a421b6c5a16d4d33546cd1c0096df86bd0f8baf" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindow.cs", + "sha256": "1a7cb750c1de6903b22a874894de0af2cb40b4cd9c0aee761074e11695693d64" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindowDatabase.cs.meta", + "sha256": "d10c55063a5f63f3103c219d598bc22c8e0e72af3be4bcbb6ea60c995c53ca99" + }, + { + "path": "skills/assets/unity-package/Editor/EditorServerWindowData.cs", + "sha256": "7da7c7d7d3a303609ea15b63c8be59b73d1d57d58e8cc0ad703448d4639af139" + }, + { + "path": "skills/assets/unity-package/Editor/EditorToolbarExtension.cs", + "sha256": "cc1fd25725f85eeee70e293b20cd8856fe010987e27b3a7db1922cecde993e00" + }, + { + "path": "skills/assets/unity-package/Editor/UnityEditorToolkit.Editor.asmdef", + "sha256": "b0fd71d2040be7fde56b484adf17c52fd160d3e85f826ce6815f355fca64a659" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Migrations.meta", + "sha256": "f6d32bdfc6b3f1a14595a1676e940cfd811cf9fd211f5d50276da5979c63c793" + }, + { + "path": "skills/assets/unity-package/Editor/Database/DatabaseManager.cs.meta", + "sha256": "06f630418e7e4790fba9154cef57687d48ac509fecead8f17ef25636a6c80d0f" + }, + { + "path": "skills/assets/unity-package/Editor/Database/MigrationRunner.cs", + "sha256": "e1af75e39ca5295b020115deab21e725f41c0aa74b902d7804b7f206a3672957" + }, + { + "path": "skills/assets/unity-package/Editor/Database/SQLiteConnector.cs.meta", + "sha256": "1c6227a1243ecc58f9e97490d37a4dde55d62767b094920836b17941661dffb8" + }, + { + "path": "skills/assets/unity-package/Editor/Database/DatabaseManager.cs", + "sha256": "5879f2eee4269efed18c3de12c1e9d83a15267fcfabb422fdfe7c2f3954460bd" + }, + { + "path": "skills/assets/unity-package/Editor/Database/DatabaseConfig.cs.meta", + "sha256": "2f51841473eddbbb7f7ca4ba759a1fb00e1f3d0f58e23469a6acbdc4e6402dda" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Queries.meta", + "sha256": "c6e158f7770889ff5058a54b3ae03f04bbb6f3d9946321f4c9050ea415f22b03" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands.meta", + "sha256": "651ea58d24aa84c16d2f8e442209fc3e004c1feeca8f03e79f78add4e6340d63" + }, + { + "path": "skills/assets/unity-package/Editor/Database/SQLiteConnector.cs", + "sha256": "e0091c67215054e0a2917ab9961e45c21453fa18eaabdfe522f1b6cc6786eb2e" + }, + { + "path": "skills/assets/unity-package/Editor/Database/SyncManager.cs.meta", + "sha256": "f004ed7aa90826d4db03224f1732a2e670835fc08c8b8d9bbd3eb562e48b210c" + }, + { + "path": "skills/assets/unity-package/Editor/Database/SyncManager.cs", + "sha256": "1659ce4e8c2269d8b9b80833b52b7f8dbde05d52e36923bdd05d316318c9b2d6" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Setup.meta", + "sha256": "830c8376d22451ae094078251adbd87550641545cb59588f06d606396ad8d1c7" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Models.meta", + "sha256": "2857e5b0163e1b03e89e084bb9a7e29c3e23778a1e9389c585417f74d4e0244e" + }, + { + "path": "skills/assets/unity-package/Editor/Database/DatabaseConfig.cs", + "sha256": "a31c1467024a4745a941084b4dd5835e12e71f34158d00fb50f7befb5f5ed0c4" + }, + { + "path": "skills/assets/unity-package/Editor/Database/MigrationRunner.cs.meta", + "sha256": "7834e3198217f42534057b10cb05745b864c1ddd3a975e3fded7a8998b96c8c4" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql.meta", + "sha256": "45a8d3ea941a77453fd0c561278fe14b02ee5c55075e237cdcbfc668a88f4f17" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql", + "sha256": "c08766cdf0258d8b9bdda09dadc66ab6005888c5ef5fc3101000c8bd84184394" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql.meta", + "sha256": "43173493c54fffd79fc800f39000c44025459ea4667c7c0c333d440129381198" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql", + "sha256": "e752783cdb5cac8cd2b4044281fcf4de2f63a1e35684ec4a408537f9107f511a" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs", + "sha256": "fdc15778f39c03e319e9580a782344e9170ec74df515a9a82b8a637421dbc30b" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs.meta", + "sha256": "d1050c7b1188a1a3398d0b89a856dbd24d3d8374690bd51ab1a5ae9c5f278f94" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs", + "sha256": "4e08b66321c41085b706c91d5bf8acd1846f5b9ccf6e24fabfcbfaf3aa0820fc" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs.meta", + "sha256": "68386fa084616660713ffcd252de0c26825fc0bd47080024c46f54ffbcf7568e" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Queries/.gitkeep", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Models/.gitkeep", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs", + "sha256": "10d28185c19074a33494394e3b156de9ebd942b78508fb6f8e45f01bf151db31" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs.meta", + "sha256": "e86d702481bbf936387a6c3014a8e6ca638c29e4c355cf469e4bf1e2da629a66" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs.meta", + "sha256": "d2559abb82df08eb644bb24497bc0974f8ce0cd073c25c497d752ab65e41f52c" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs.meta", + "sha256": "d1f48187ee8038b497a6d7295160d213ef34f0d980ad0dc3f150eeca7009a446" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs", + "sha256": "adf6066896e8dca126062a02785a346df144727cda00b812a773644a5992d302" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/ICommand.cs", + "sha256": "cf78130807838a730404320520952f31e0e37dd4ede73c0ea48ea7fd2276e18e" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs.meta", + "sha256": "31989a5b94d630f31182024280f7666523b4a927441feb0dbd1aaa4ea5431652" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs", + "sha256": "c735b4cf71044b4ff397c3b4c58d94134fb7c0e87aa186d36cb17fa724735059" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs", + "sha256": "d21b94c6fc729b9414e81909efa6e1440eba3b42999153527f6351087284d609" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/ICommand.cs.meta", + "sha256": "8e156f808b420a5d1578aaa0a5303d9d2e661ad71401796ad5fdd9b557aef2bb" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs", + "sha256": "86b6853879e047a86395e3b1d69d89f41e4e6ad4f18f8a40073c1ee3d207f4ef" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs", + "sha256": "e47734a7958638f221ffa2adec9dd5085ff6f74ed0737d7a6eed483131aefb25" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs.meta", + "sha256": "3d8048dc6c8f519fa32e5a8c2b2ff9fbda39fa03883ea5685f45fd5e1998647e" + }, + { + "path": "skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs.meta", + "sha256": "eaf3903ea4a87452b270ea7768f660f22cc9791877676dff4bd33b957883fc96" + }, + { + "path": "skills/assets/unity-package/Editor/Server/ServerStatus.cs.meta", + "sha256": "518e14de16f2ea29bf6ad1cdf1b20a04ea2b5977d0757b2c81a3a2d0f67faa8f" + }, + { + "path": "skills/assets/unity-package/Editor/Server/EditorWebSocketServer.cs", + "sha256": "53602de3e5d68602fb9a704d5abc0efb33b1761b9454600864863d4fd8426320" + }, + { + "path": "skills/assets/unity-package/Editor/Server/EditorWebSocketServer.cs.meta", + "sha256": "847b754e7cdecc0c1e0ec5344e224f5b452c5990889c14f4c515b7ce3ff9cfce" + }, + { + "path": "skills/assets/unity-package/Editor/Server/ServerStatus.cs", + "sha256": "928d7b6f2558d4930f033c1a36cfc71c4b78c4ac9079ece9118d9d2db00b185d" + }, + { + "path": "skills/assets/unity-package/Editor/Utils/EditorMainThreadDispatcher.cs", + "sha256": "158fa4ec66f2984becfc3cc66726a9ece9a4bb356adc6031a63606b8931f18cd" + }, + { + "path": "skills/assets/unity-package/Editor/Utils/Logger.cs", + "sha256": "41034e33939e1f2a8d891a15528a683b97af8c643eafea4ab7fed72617567e43" + }, + { + "path": "skills/assets/unity-package/Editor/Utils/Logger.cs.meta", + "sha256": "b18126f03458c9b48b4572de15756cc7cef4291a028e0cb1901440c1e4bd42f7" + }, + { + "path": "skills/assets/unity-package/Editor/Utils/ResponseQueue.cs.meta", + "sha256": "35ff239e30b894b732d207d9afee79ff8755b2934284003fd86671ed342a68e0" + }, + { + "path": "skills/assets/unity-package/Editor/Utils/ResponseQueue.cs", + "sha256": "c4b777e6012fbb8bf2f8180ef939f148ebc23d56d72fcbb3649a562789ab196b" + }, + { + "path": "skills/assets/unity-package/Editor/Utils/EditorMainThreadDispatcher.cs.meta", + "sha256": "5a84c7fc7dbc642ae6b958dc52dca3393e7e207c785932211b3b1f6b3d4321c1" + }, + { + "path": "skills/assets/unity-package/Editor/Protocol/JsonRpcResponse.cs", + "sha256": "0be79db464d8615e7e617943b759556a0f8ad0596cf53fecb8339108b1b241a0" + }, + { + "path": "skills/assets/unity-package/Editor/Protocol/JsonRpcRequest.cs", + "sha256": "1292cb3b95858413c3f3291c157c1e738400cd558b66af029c8f38ec0c0c88be" + }, + { + "path": "skills/assets/unity-package/Editor/Protocol/JsonRpcResponse.cs.meta", + "sha256": "c8cef431151996e5516fb4fedb69b1892f7522e16515eb4bd08cbbd2554bc41a" + }, + { + "path": "skills/assets/unity-package/Editor/Protocol/JsonRpcError.cs.meta", + "sha256": "8468b5b051edc61d18e930a867d8b62a869631df9469c7a799f6e0469da24f39" + }, + { + "path": "skills/assets/unity-package/Editor/Protocol/JsonRpcRequest.cs.meta", + "sha256": "cdb46ab409dee414337eb56a6bca9c677cd02ff51c03c9eb9ecb9a00931daa67" + }, + { + "path": "skills/assets/unity-package/Editor/Protocol/JsonRpcError.cs", + "sha256": "5e02d1af93d913cac7d39ce499c6198ccb15f83b1bcd1a65240965ded1b8281c" + }, + { + "path": "skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs.meta", + "sha256": "f862fe0d7c360233b8427fccfbf1b4256ea819583f878bc44584d4141175f1be" + }, + { + "path": "skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs", + "sha256": "bac38ab95d7f4c37ccce2f44338b9c6ef28e50f80e97567bde9df899afcf56d8" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/HierarchyHandler.cs.meta", + "sha256": "3a2bc02b0ed0ed9c595fc38999ef12b3210149f39bbc1d8fea1897a147370197" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/SyncHandler.cs", + "sha256": "3aa06f9b6def66a3c78677504644e8e16fa29d968efb3af2e3ca524b12211028" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/ConsoleHandler.cs", + "sha256": "bc08a2f22272f5dda9701d9924d3bcfedc02193fa18640d5b27c7024f4a85adb" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/TransformHistoryHandler.cs", + "sha256": "3bd98012463be7814b3251622fb03c41bf55274f02d7cad93e3c8f2037c7de3e" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/ChainHandler.cs.meta", + "sha256": "398112cc7f36792ffd11da14a7614335e0e02a34213edb97a610f51af77a7cf9" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/MaterialHandler.cs", + "sha256": "42053ff7a457a981244087f185502f6252062b153c6cd64602007543f0f63cc5" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/TransformHandler.cs.meta", + "sha256": "f267629159572a7066f9637698f0d224e0a9a3248885a625cc196786559ad070" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/AssetHandler.cs.meta", + "sha256": "124cb15da656faa4c9a615376a22617ac768b1ff14fdc27f376e5a0bb6da6bd0" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/SyncHandler.cs.meta", + "sha256": "9a713e7a4b67b9b4828db72f753fc6ab0b5808c1e75b912c8e0ac4db827b7ac3" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/ComponentHandler.cs", + "sha256": "b52ab282150974323f07ae6c8346360fd1f82026b2351df292d29c2a1e238720" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/SnapshotHandler.cs.meta", + "sha256": "a5e080a34ef83c8d71719dc444812e3018a597de0becc0da8946e23a24681980" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/AssetHandler.cs", + "sha256": "0fac9ec1840bbbd7782b4c83de89054e8ba10129fa2a61bddba81c1cc2d3e71a" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/ComponentHandler.cs.meta", + "sha256": "c05942b4068b907051868e29b57d794e697fda8ee34940119df801cf1f6dfe4e" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/SceneHandler.cs.meta", + "sha256": "af35c9743c4a38561a25a5d99ae350aec3ce9e49f6cb0d2f98dc2d030cfc50d8" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/HierarchyHandler.cs", + "sha256": "176027193d8083df500b7dad1e51c2e0ded369f924fd461fa75dd6727ea3678d" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/PrefsHandler.cs.meta", + "sha256": "397132a1d3a5784c0960679b33a5ae2ff3d14bc3182582b428334ae1d36abfc8" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/BaseHandler.cs.meta", + "sha256": "1b42adf5e80dbc19dca09205354c0388adf4059afc5abc68486d967131ca66b5" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/MenuHandler.cs.meta", + "sha256": "9ec180346e0d3d44d143762db1908f92fcb4e809c8cea46f97bba8d9a06475a7" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/WaitHandler.cs.meta", + "sha256": "aadebe5e1dfe06335f3f19f23b307ceb915a01bb61dc2483170e0adcc875a208" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/TransformHistoryHandler.cs.meta", + "sha256": "a9fbb640e48da2ac48aa6778de35f0059f3fbddadcf48ca5bc2e083799b602a2" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/MenuHandler.cs", + "sha256": "4f311c706f06cb27469927322924498bf821e1f4f3592844c9dda438f66ed299" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/DatabaseHandler.cs", + "sha256": "5158e711e6876cb514d6a2f9841a2917fa095e0b884fd5793b7f456f760270b7" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/MaterialHandler.cs.meta", + "sha256": "87b0a3932c0d4f05740934a71c2a64bdff3f7b663af2cfe8ff07cb3a86098eea" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/GameObjectHandler.cs.meta", + "sha256": "87b21b3d190e560b5ea0584b9495b8d2285686168242aca4d9b37c2c38f6da72" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/SceneHandler.cs", + "sha256": "9bc6664427b5115b2571fb52e666458fd57aed126f846c083802ddc3c60cd840" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/BaseHandler.cs", + "sha256": "527a6547c56da7105f2d7f7b902e4fc23c1493e008ac3423654b66288398c082" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/EditorHandler.cs.meta", + "sha256": "77593160aafab6328d39affbde26fabf84dc9b975929562e697124fcbb3c30ed" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/SnapshotHandler.cs", + "sha256": "0e6dfc902841b78dea6b3e64a094d717333c3566ec773183200e8b3f56feda37" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/TransformHandler.cs", + "sha256": "486f2760f13acf495335ff0ab84cc0317020a5cec3ce2d83b768f439366e9e3b" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/WaitHandler.cs", + "sha256": "7a28cc1724e716e3f3064b0fdc46ccf46ba1d94682e8c29ada7ac51cb1e5adeb" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/EditorHandler.cs", + "sha256": "cf1a19f03805cab6351e068cd53536f4274cc7e039f8a1ababa901ba99117a7f" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/DatabaseHandler.cs.meta", + "sha256": "7e2c68eb22348c32e4900a0ccc5b98f23c0cb4343dee97aeb4b8e69edd6e71d7" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/PrefsHandler.cs", + "sha256": "f9ec470285af1cfeb60ef970dbe7aa40a31a99d7ec08adf26931d67ec0ae264b" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/ConsoleHandler.cs.meta", + "sha256": "1903b99b0dfa45c5cd585960a4837bfc640b7271195b5b636a1dffa40bca3fd7" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/PrefabHandler.cs", + "sha256": "1079f5fe484b11b59e5208b16499687801983f51462309b1fee423b9efdb7917" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/AnalyticsHandler.cs.meta", + "sha256": "ee7b62d5de80f8130c5cb4441d00d89fb91d04cd930e374c5757907ac6815fbd" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/GameObjectHandler.cs", + "sha256": "874e5e6de61143d07ff4aef7f1cfed55f527108781bc367a8cea5fc4201921dc" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/ChainHandler.cs", + "sha256": "a155d82e2aa126d0decb6df0481ebdc4c1a4f8a5c4146fe2fdaa2cf32d53535c" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/AnimationHandler.cs", + "sha256": "f2ebd3d9e6e7871e4cebbe5a9fe6bc1d35a51612782f3dc38f54b2c9d3f7983a" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/PrefabHandler.cs.meta", + "sha256": "b28f4b474882ca2c285c6fb2da8490119f2348676ca7388b8ee5beb49c0ec887" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/AnalyticsHandler.cs", + "sha256": "44688dd67716ed5e240c445508201678332d96760e9451f452f8fb40a3c248f6" + }, + { + "path": "skills/assets/unity-package/Editor/Handlers/AnimationHandler.cs.meta", + "sha256": "7af8fe3d07db755d69f6af812ea1236cac23f13180aa4c3f52f818f6cf219ebd" + } + ], + "dirSha256": "68ef93451a78795c55a9f6b79a9d9f69f9559382bd97dde8918f3a36627a8478" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/SKILL.md b/skills/SKILL.md new file mode 100644 index 0000000..332ce31 --- /dev/null +++ b/skills/SKILL.md @@ -0,0 +1,387 @@ +--- +name: unity-editor-toolkit +description: | + Unity Editor control and automation, WebSocket-based real-time communication. 유니티에디터제어및자동화, WebSocket기반실시간통신. + + Features/기능: GameObject control 게임오브젝트제어, Transform manipulation 트랜스폼조작, Component management 컴포넌트관리, Scene management 씬관리, SQLite database integration SQLite데이터베이스통합, GUID-based persistence GUID기반영구식별, Multi-scene synchronization 멀티씬동기화, Command Pattern with Undo/Redo 명령패턴실행취소재실행, Menu execution 메뉴실행, ScriptableObject management 스크립터블오브젝트관리, Array/List manipulation 배열리스트조작, All field types support 모든필드타입지원, Material/Rendering 머티리얼/렌더링, Prefab system 프리팹시스템, Asset Database 애셋데이터베이스, Animation 애니메이션, Physics 물리, Console logging 콘솔로깅, EditorPrefs management 에디터프리퍼런스관리, Editor automation 에디터자동화, Build pipeline 빌드파이프라인, Lighting 라이팅, Camera 카메라, Audio 오디오, Navigation 네비게이션, Particles 파티클, Timeline 타임라인, UI Toolkit, Profiler 프로파일러, Test Runner 테스트러너. + + Protocol 프로토콜: JSON-RPC 2.0 over WebSocket (port 9500-9600). 500+ commands 명령어, 25 categories 카테고리. Real-time bidirectional communication 실시간양방향통신. + + Security 보안: Defense-in-depth 심층방어 (path traversal protection 경로순회방지, command injection defense 명령어인젝션방어, JSON injection prevention JSON인젝션방지, SQL injection prevention SQL인젝션방지, transaction safety 트랜잭션안전성). Localhost-only connections 로컬호스트전용. Cross-platform 크로스플랫폼 (Windows, macOS, Linux). +--- + +## Purpose + +Unity Editor Toolkit enables comprehensive Unity Editor automation and control from Claude Code. It provides: + +- **Extensive Command Coverage**: 500+ commands spanning 25 Unity Editor categories +- **Real-time Communication**: Instant bidirectional WebSocket connection (JSON-RPC 2.0) +- **SQLite Database Integration**: Real-time GameObject synchronization with GUID-based persistence + - **GUID-based Identification**: Persistent GameObject tracking across Unity sessions + - **Multi-scene Support**: Synchronize all loaded scenes simultaneously (1s interval) + - **Command Pattern**: Undo/Redo support for database operations + - **Auto Migration**: Automatic schema migration system + - **Batch Operations**: Efficient bulk inserts, updates, and deletes (500 objects/batch) +- **Menu Execution**: Run Unity Editor menu items programmatically (Window, Assets, Edit, GameObject menus) +- **ScriptableObject Management**: Complete CRUD operations with array/list support and all field types + - **Array/List Operations**: Add, remove, get, clear elements with nested access (`items[0].name`) + - **All Field Types**: Integer, Float, String, Boolean, Vector*, Color, Quaternion, Bounds, AnimationCurve, ObjectReference, and more + - **Nested Property Traversal**: Access deeply nested fields with dot notation and array indices +- **Deep Editor Integration**: GameObject/hierarchy, transforms, components, scenes, materials, prefabs, animation, physics, lighting, build pipeline, and more +- **Security First**: Multi-layer defense against injection attacks (SQL, command, JSON, path traversal) and unauthorized access +- **Production Ready**: Cross-platform support with robust error handling and logging + +**Always run scripts with `--help` first** to see usage. DO NOT read the source until you try running the script first and find that a customized solution is abslutely necessary. These scripts can be very large and thus pollute your context window. They exist to be called directly as black-box scripts rather than ingested into your context window. + +--- + +## 📚 문서 우선 원칙 (필수) + +**⚠️ CRITICAL**: Unity Editor Toolkit skill을 사용할 때는 **반드시 다음 순서를 따르세요:** + +### 1️⃣ Reference 문서 확인 (필수) +명령어를 사용하기 전에 **반드시** `skills/references/` 폴더의 해당 문서를 읽으세요: +- **[COMMANDS.md](./references/COMMANDS.md)** - 모든 명령어의 카테고리 및 개요 +- **Category-specific docs** - 사용할 명령어의 카테고리 문서: + - [Component Commands](./references/COMMANDS_COMPONENT.md) - comp list/add/remove/enable/disable/get/set/inspect/move-up/move-down/copy + - [GameObject Commands](./references/COMMANDS_GAMEOBJECT_HIERARCHY.md) - go find/create/destroy/set-active/set-parent/get-parent/get-children + - [Transform Commands](./references/COMMANDS_TRANSFORM.md) - tf get/set-position/set-rotation/set-scale + - [Scene Commands](./references/COMMANDS_SCENE.md) - scene current/list/load/new/save/unload/set-active + - [Console Commands](./references/COMMANDS_CONSOLE.md) - console logs/clear + - [EditorPrefs Commands](./references/COMMANDS_PREFS.md) - prefs get/set/delete/list/clear/import + - [Other Categories](./references/COMMANDS.md) - 추가 명령어 카테고리 + +### 2️⃣ `--help` 실행 +```bash +# 모든 명령어 확인 +cd && node .unity-websocket/uw --help + +# 특정 명령어의 옵션 확인 +cd && node .unity-websocket/uw --help +``` + +### 3️⃣ 예제 실행 +reference 문서의 **Examples 섹션**을 참고하여 명령어를 실행하세요. + +### 4️⃣ 소스 코드 읽기 (최후의 수단) +- reference 문서와 --help만으로는 해결 안 될 때만 소스 코드를 읽으세요 +- 소스 코드는 컨텍스트 윈도우를 많이 차지하므로 가능하면 피하세요 + +**이 순서를 무시하면:** +- ❌ 명령어 사용법을 잘못 이해할 수 있음 +- ❌ 옵션을 놓쳐서 원하지 않는 결과가 나올 수 있음 +- ❌ 컨텍스트 윈도우를 낭비할 수 있음 + +--- + +## When to Use + +Use Unity Editor Toolkit when you need to: + +1. **Automate Unity Editor Tasks** + - Create and manipulate GameObjects, components, and hierarchies + - Configure scenes, materials, and rendering settings + - Control animation, physics, and particle systems + - Manage assets, prefabs, and build pipelines + +2. **Real-time Unity Testing** + - Monitor console logs and errors during development + - Query GameObject states and component properties + - Test scene configurations and gameplay logic + - Debug rendering, physics, or animation issues + +3. **Batch Operations** + - Create multiple GameObjects with specific configurations + - Apply material/shader changes across multiple objects + - Setup scene hierarchies from specifications + - Automate repetitive Editor tasks + +4. **Menu and Editor Automation** + - Execute Unity Editor menu items programmatically (`menu run "Window/General/Console"`) + - Open editor windows and tools via command line + - Automate asset refresh, reimport, and build operations + - Query available menu items with wildcard filtering + +5. **ScriptableObject Management** + - Create and configure ScriptableObject assets programmatically + - Read and modify all field types (Vector, Color, Quaternion, AnimationCurve, etc.) + - Manipulate arrays/lists with full CRUD operations + - Access nested properties with array index notation (`items[0].stats.health`) + - Query ScriptableObject types and inspect asset metadata + +6. **Database-Driven Workflows** + - Persistent GameObject tracking across Unity sessions with GUID-based identification + - Real-time synchronization of all loaded scenes to SQLite database + - Analytics and querying of GameObject hierarchies and properties + - Undo/Redo support for database operations via Command Pattern + - Efficient batch operations (500 objects/batch) for large scene management + +7. **CI/CD Integration** + - Automated builds with platform-specific settings + - Test Runner integration for unit/integration tests + - Asset validation and integrity checks + - Build pipeline automation + +## Prerequisites + +### Unity Project Setup + +1. **Install Unity Editor Toolkit Server Package** + - Via Unity Package Manager (Git URL or local path) + - Requires Unity 2020.3 or higher + - Package location: `skills/assets/unity-package` + +2. **Configure WebSocket Server** + - Open Unity menu: `Tools > Unity Editor Toolkit > Server Window` + - Plugin scripts path auto-detected from `~/.claude/plugins/...` + - Click "Install CLI" to build WebSocket server (one-time setup) + - Server starts automatically when Unity Editor opens + +3. **Database Setup** (Optional) + - In the Server window, switch to "Database" tab + - Click "Connect" to initialize SQLite database + - Database file location: `{ProjectRoot}/.unity-websocket/unity-editor.db` + - Click "Start Sync" to enable real-time GameObject synchronization (1s interval) + - **GUID Components**: GameObjects are automatically tagged with persistent GUIDs + - **Multi-scene**: All loaded scenes are synchronized automatically + - **Analytics**: View sync stats, database health, and Undo/Redo history + +4. **Server Status** + - Port: Auto-assigned from range 9500-9600 + - Status file: `{ProjectRoot}/.unity-websocket/server-status.json` + - CLI automatically detects correct port from this file + +5. **Dependencies** + - websocket-sharp (install via package installation scripts) + - Newtonsoft.Json (Unity's built-in version) + - Cysharp.UniTask (for async/await database operations) + - SQLite-net (embedded SQLite database) + +### Claude Code Plugin + +The Unity Editor Toolkit plugin provides CLI commands for Unity Editor control. + +## Core Workflow + +### 1. Connection + +Unity Editor Toolkit CLI automatically: + +- Detects Unity project via `.unity-websocket/server-status.json` +- Reads port information from status file (9500-9600 range) +- Connects to WebSocket server if Unity Editor is running + +### 2. Execute Commands + +⚠️ **Before executing ANY command, check the reference documentation for your command category** (see "📚 문서 우선 원칙" section above). + +Unity Editor Toolkit provides 40+ commands across 12+ categories. All commands run from the Unity project root: + +```bash +cd && node .unity-websocket/uw [options] +``` + +**Available Categories** (Implemented): + +| # | Category | Commands | Reference | +|---|----------|----------|-----------| +| 1 | Connection & Status | 1 | [COMMANDS_CONNECTION_STATUS.md](./references/COMMANDS_CONNECTION_STATUS.md) | +| 2 | GameObject & Hierarchy | 8 | [COMMANDS_GAMEOBJECT_HIERARCHY.md](./references/COMMANDS_GAMEOBJECT_HIERARCHY.md) | +| 3 | Transform | 4 | [COMMANDS_TRANSFORM.md](./references/COMMANDS_TRANSFORM.md) | +| 4 | **Component** ✨ | 10 | [COMMANDS_COMPONENT.md](./references/COMMANDS_COMPONENT.md) | +| 5 | Scene Management | 7 | [COMMANDS_SCENE.md](./references/COMMANDS_SCENE.md) | +| 6 | Asset Database & Editor | 3 | [COMMANDS_EDITOR.md](./references/COMMANDS_EDITOR.md) | +| 7 | Console & Logging | 2 | [COMMANDS_CONSOLE.md](./references/COMMANDS_CONSOLE.md) | +| 8 | EditorPrefs Management | 6 | [COMMANDS_PREFS.md](./references/COMMANDS_PREFS.md) | +| 9 | Wait Commands | 4 | [COMMANDS_WAIT.md](./references/COMMANDS_WAIT.md) | +| 10 | Chain Commands | 2 | [COMMANDS_CHAIN.md](./references/COMMANDS_CHAIN.md) | +| 11 | Menu Execution | 2 | [COMMANDS_MENU.md](./references/COMMANDS_MENU.md) | +| 12 | Asset Management | 9 | [COMMANDS_ASSET.md](./references/COMMANDS_ASSET.md) | + +**Usage:** + +```bash +cd && node .unity-websocket/uw [options] +``` + +**Required: Check Documentation** + +```bash +# 1. 먼저 명령어 카테고리의 reference 문서를 읽으세요 +# 예: Component 명령어 사용 → skills/references/COMMANDS_COMPONENT.md 읽기 + +# 2. --help로 명령어 옵션 확인 +cd && node .unity-websocket/uw --help +cd && node .unity-websocket/uw --help + +# 3. reference 문서의 예제를 참고하여 실행 +``` + +**📖 Complete Documentation by Category** + +**Required Reading**: Before using any command, read the **Category-specific reference document**: +- 🔴 **MUST READ FIRST** - [COMMANDS.md](./references/COMMANDS.md) - Overview and command roadmap +- 🔴 **MUST READ** - Category-specific docs (links in the table above) + - [Component Commands](./references/COMMANDS_COMPONENT.md) - **NEW**: comp list/add/remove/enable/disable/get/set/inspect/move-up/move-down/copy + - [GameObject Commands](./references/COMMANDS_GAMEOBJECT_HIERARCHY.md) - go find/create/destroy/set-active/set-parent/get-parent/get-children + - [Transform Commands](./references/COMMANDS_TRANSFORM.md) - tf get/set-position/set-rotation/set-scale + - [Scene Commands](./references/COMMANDS_SCENE.md) - scene current/list/load/new/save/unload/set-active + - [Console Commands](./references/COMMANDS_CONSOLE.md) - console logs/clear + - [EditorPrefs Commands](./references/COMMANDS_PREFS.md) - prefs get/set/delete/list/clear/import + - [Other Categories](./references/COMMANDS.md) - Full list with all categories + +### 3. Check Connection Status + +```bash +# Verify WebSocket connection +cd && node .unity-websocket/uw status + +# Use custom port +cd && node .unity-websocket/uw --port 9301 status +``` + +### 4. Complex Workflows + +**Create and configure GameObject:** +```bash +cd && node .unity-websocket/uw go create "Enemy" && \ +cd && node .unity-websocket/uw tf set-position "Enemy" "10,0,5" && \ +cd && node .unity-websocket/uw tf set-rotation "Enemy" "0,45,0" +``` + +**Load scene and activate GameObject:** +```bash +cd && node .unity-websocket/uw scene load "Level1" && \ +cd && node .unity-websocket/uw go set-active "Boss" true +``` + +**Batch GameObject creation:** +```bash +for i in {1..10}; do + cd && node .unity-websocket/uw go create "Cube_$i" && \ + cd && node .unity-websocket/uw tf set-position "Cube_$i" "$i,0,0" +done +``` + +**Wait for compilation then execute:** +```bash +# Make code changes, then wait for compilation to finish +cd && node .unity-websocket/uw wait compile && \ +cd && node .unity-websocket/uw editor refresh +``` + +**Chain multiple commands sequentially:** +```bash +# Execute commands from JSON file +cd && node .unity-websocket/uw chain execute commands.json + +# Execute commands inline +cd && node .unity-websocket/uw chain exec \ + "GameObject.Create:name=Player" \ + "GameObject.SetActive:instanceId=123,active=true" + +# Continue execution even if some commands fail +cd && node .unity-websocket/uw chain exec \ + "Editor.Refresh" \ + "GameObject.Find:path=InvalidPath" \ + "Console.Clear" \ + --continue-on-error +``` + +**CI/CD Pipeline workflow:** +```bash +#!/bin/bash +cd /path/to/unity/project + +# Cleanup +node .unity-websocket/uw.js chain exec "Console.Clear" "Editor.Refresh" + +# Wait for compilation +node .unity-websocket/uw.js wait compile + +# Run tests (example) +node .unity-websocket/uw.js chain exec \ + "Scene.Load:name=TestScene" \ + "GameObject.Find:path=TestRunner" \ + "Console.Clear" +``` + +## Best Practices + +1. **Always Verify Connection** + - Run `cd && node .unity-websocket/uw status` before executing commands + - Ensure Unity Editor is running and server component is active + +2. **Use Hierarchical Paths** + - Prefer full paths for nested GameObjects: `"Environment/Terrain/Trees"` + - Avoids ambiguity when multiple GameObjects share the same name + +3. **Monitor Console Logs** + - Use `cd && node .unity-websocket/uw console logs --errors-only` to catch errors during automation + - Clear console before running automation scripts for clean logs + +4. **Batch Operations Carefully** + - Add delays between commands if creating many GameObjects + - Consider Unity Editor performance limitations + +5. **Connection Management** + - Unity Editor Toolkit uses localhost-only connections (127.0.0.1) + - Port range limited to 9500-9600 to avoid conflicts with other tools + +6. **Error Handling** + - Commands return JSON-RPC error responses for invalid operations + - Check exit codes and error messages in automation scripts + +7. **Port Management** + - Default port 9500 works for most projects + - Use `--port` flag if running multiple Unity Editor instances + - Plugin avoids conflicts with Browser Pilot (9222-9322) and Blender Toolkit (9400-9500) + +8. **Wait Commands Usage** + - Use `wait compile` after making code changes to ensure compilation finishes + - Use `wait playmode enter/exit` for play mode synchronization in automated tests + - Use `wait sleep` to add delays between commands when needed + - Note: Wait commands have delayed responses (default 5-minute timeout) + - Domain reload automatically cancels all pending wait requests + +9. **Chain Commands Best Practices** + - Use chain for sequential command execution with automatic error handling + - Default behavior: stop on first error (use `--continue-on-error` to override) + - Wait commands are NOT supported in chain (use separate wait commands) + - Use JSON files for complex multi-step workflows + - Use inline exec for quick command sequences + +10. **Development Roadmap Awareness** + - **Phase 1 (Current)**: GameObject, Transform, Scene, Console, Wait, Chain - 26 commands + - **Phase 2+**: Component, Material, Prefab, Animation, Physics, Build - 474+ commands coming soon + - See full roadmap in [COMMANDS.md](./references/COMMANDS.md) + +## References + +Detailed documentation available in the `references/` folder: + +- **[QUICKSTART.md](../QUICKSTART.md)** - Quick setup and first commands (English) +- **[QUICKSTART.ko.md](../QUICKSTART.ko.md)** - Quick setup guide (Korean) +- **[COMMANDS.md](./references/COMMANDS.md)** - Complete 500+ command roadmap (English) +- **Implemented Command Categories:** + - [Connection & Status](./references/COMMANDS_CONNECTION_STATUS.md) + - [GameObject & Hierarchy](./references/COMMANDS_GAMEOBJECT_HIERARCHY.md) + - [Transform](./references/COMMANDS_TRANSFORM.md) + - [Scene Management](./references/COMMANDS_SCENE.md) + - [Asset Database & Editor](./references/COMMANDS_EDITOR.md) + - [Console & Logging](./references/COMMANDS_CONSOLE.md) + - [EditorPrefs Management](./references/COMMANDS_PREFS.md) + - [Wait Commands](./references/COMMANDS_WAIT.md) + - [Chain Commands](./references/COMMANDS_CHAIN.md) +- **[API_COMPATIBILITY.md](../API_COMPATIBILITY.md)** - Unity version compatibility (2020.3 - Unity 6) +- **[TEST_GUIDE.md](../TEST_GUIDE.md)** - Unity C# server testing guide (English) +- **[TEST_GUIDE.ko.md](../TEST_GUIDE.ko.md)** - Unity C# server testing guide (Korean) + +Unity C# server package available in `assets/unity-package/` - install via Unity Package Manager once released. + +--- + +**Status**: 🧪 Experimental - Phase 1 (26 commands implemented) +**Unity Version Support**: 2020.3 - Unity 6 +**Protocol**: JSON-RPC 2.0 over WebSocket +**Port Range**: 9500-9600 (auto-assigned) diff --git a/skills/assets/unity-package/Editor.meta b/skills/assets/unity-package/Editor.meta new file mode 100644 index 0000000..b94c392 --- /dev/null +++ b/skills/assets/unity-package/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: deb67eda7d4bc4d4e85a0ae423f65903 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/skills/assets/unity-package/Editor/Attributes.meta b/skills/assets/unity-package/Editor/Attributes.meta new file mode 100644 index 0000000..508d7db --- /dev/null +++ b/skills/assets/unity-package/Editor/Attributes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 37d6465efcdcc9349bf098e0b33cfffc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs b/skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs new file mode 100644 index 0000000..db1e2e7 --- /dev/null +++ b/skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs @@ -0,0 +1,38 @@ +using System; + +namespace UnityEditorToolkit.Editor.Attributes +{ + /// + /// Marks a static method as executable via CLI + /// Only methods with this attribute can be executed through Editor.Execute command + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public class ExecutableMethodAttribute : Attribute + { + /// + /// CLI command name (e.g., "reinstall-cli") + /// + public string CommandName { get; } + + /// + /// Description of what this method does + /// + public string Description { get; } + + /// + /// Mark a method as executable via CLI + /// + /// CLI command name (kebab-case recommended) + /// Human-readable description + public ExecutableMethodAttribute(string commandName, string description = "") + { + if (string.IsNullOrWhiteSpace(commandName)) + { + throw new ArgumentException("Command name cannot be null or empty", nameof(commandName)); + } + + CommandName = commandName; + Description = description; + } + } +} diff --git a/skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs.meta b/skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs.meta new file mode 100644 index 0000000..6e81f94 --- /dev/null +++ b/skills/assets/unity-package/Editor/Attributes/ExecutableMethodAttribute.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f459669cdec8d5b469285409d8ea3467 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database.meta b/skills/assets/unity-package/Editor/Database.meta new file mode 100644 index 0000000..87e630d --- /dev/null +++ b/skills/assets/unity-package/Editor/Database.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9d252d66841b3354c9e5d3a0b149edbc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/skills/assets/unity-package/Editor/Database/Commands.meta b/skills/assets/unity-package/Editor/Database/Commands.meta new file mode 100644 index 0000000..9bb8d3d --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9809e375b6992d4fab11953cd09b301 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs b/skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs new file mode 100644 index 0000000..bea70b4 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs @@ -0,0 +1,157 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database.Commands +{ + /// + /// Command Pattern 기본 추상 클래스 + /// 공통 기능 구현 + /// + public abstract class CommandBase : ICommand + { + #region ICommand Properties + public string CommandId { get; protected set; } + public string CommandName { get; protected set; } + public DateTime ExecutedAt { get; protected set; } + public virtual bool CanPersist => true; + #endregion + + #region Constructor + protected CommandBase(string commandName) + { + CommandId = Guid.NewGuid().ToString(); + CommandName = commandName; + ExecutedAt = DateTime.UtcNow; + } + #endregion + + #region Abstract Methods + /// + /// 실제 실행 로직 (파생 클래스에서 구현) + /// + protected abstract UniTask OnExecuteAsync(); + + /// + /// 실제 Undo 로직 (파생 클래스에서 구현) + /// + protected abstract UniTask OnUndoAsync(); + + /// + /// 실제 Redo 로직 (파생 클래스에서 구현) + /// + protected abstract UniTask OnRedoAsync(); + #endregion + + #region ICommand Implementation + public async UniTask ExecuteAsync() + { + try + { + ToolkitLogger.LogDebug("Command", $" Executing: {CommandName}"); + ExecutedAt = DateTime.UtcNow; + bool result = await OnExecuteAsync(); + + if (result) + { + ToolkitLogger.LogDebug("Command", $" Executed successfully: {CommandName}"); + } + else + { + ToolkitLogger.LogWarning("Command", $" Execution failed: {CommandName}"); + } + + return result; + } + catch (Exception ex) + { + ToolkitLogger.LogError("Command", $" Exception during execution: {CommandName}\n{ex.Message}"); + return false; + } + } + + public async UniTask UndoAsync() + { + try + { + ToolkitLogger.LogDebug("Command", $" Undoing: {CommandName}"); + bool result = await OnUndoAsync(); + + if (result) + { + ToolkitLogger.LogDebug("Command", $" Undo successful: {CommandName}"); + } + else + { + ToolkitLogger.LogWarning("Command", $" Undo failed: {CommandName}"); + } + + return result; + } + catch (Exception ex) + { + ToolkitLogger.LogError("Command", $" Exception during undo: {CommandName}\n{ex.Message}"); + return false; + } + } + + public async UniTask RedoAsync() + { + try + { + ToolkitLogger.LogDebug("Command", $" Redoing: {CommandName}"); + bool result = await OnRedoAsync(); + + if (result) + { + ToolkitLogger.LogDebug("Command", $" Redo successful: {CommandName}"); + } + else + { + ToolkitLogger.LogWarning("Command", $" Redo failed: {CommandName}"); + } + + return result; + } + catch (Exception ex) + { + ToolkitLogger.LogError("Command", $" Exception during redo: {CommandName}\n{ex.Message}"); + return false; + } + } + #endregion + + #region Serialization + public virtual string Serialize() + { + // 기본 직렬화 (파생 클래스에서 오버라이드 가능) + return JsonUtility.ToJson(new CommandData + { + commandId = CommandId, + commandName = CommandName, + executedAt = ExecutedAt.ToString("o") + }); + } + + public virtual void Deserialize(string json) + { + // 기본 역직렬화 (파생 클래스에서 오버라이드 가능) + var data = JsonUtility.FromJson(json); + CommandId = data.commandId; + CommandName = data.commandName; + ExecutedAt = DateTime.Parse(data.executedAt); + } + #endregion + + #region Serialization Data + [Serializable] + protected class CommandData + { + public string commandId; + public string commandName; + public string executedAt; + } + #endregion + } +} diff --git a/skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs.meta b/skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs.meta new file mode 100644 index 0000000..fa9c746 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/CommandBase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4cc7fda14659f4345be98770ea61234e \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs b/skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs new file mode 100644 index 0000000..1a3fd33 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs @@ -0,0 +1,58 @@ +using System; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database.Commands +{ + /// + /// Command Factory + /// 데이터베이스에서 로드한 명령을 적절한 Command 인스턴스로 복원 + /// + public static class CommandFactory + { + /// + /// command_type과 command_data로부터 ICommand 인스턴스 생성 + /// + /// Command 타입 이름 (예: "CreateGameObjectCommand") + /// 직렬화된 JSON 데이터 + /// 복원된 ICommand 인스턴스, 실패 시 null + public static ICommand CreateFromDatabase(string commandType, string commandData) + { + try + { + switch (commandType) + { + case "CreateGameObjectCommand": + return CreateGameObjectCommand.FromJson(commandData); + + case "TransformChangeCommand": + return TransformChangeCommand.FromJson(commandData); + + // DeleteGameObjectCommand는 CanPersist = false이므로 데이터베이스에 저장되지 않음 + + default: + ToolkitLogger.LogWarning("CommandFactory", $" 알 수 없는 Command 타입: {commandType}"); + return null; + } + } + catch (Exception ex) + { + ToolkitLogger.LogError("CommandFactory", $" Command 복원 실패 - Type: {commandType}, Error: {ex.Message}"); + return null; + } + } + + /// + /// Command 타입이 지원되는지 확인 + /// + public static bool IsSupported(string commandType) + { + return commandType switch + { + "CreateGameObjectCommand" => true, + "TransformChangeCommand" => true, + _ => false + }; + } + } +} diff --git a/skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs.meta b/skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs.meta new file mode 100644 index 0000000..1a20668 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/CommandFactory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2d6bb958df2f6a242a02c7274b376c67 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs b/skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs new file mode 100644 index 0000000..4eb23ae --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs @@ -0,0 +1,514 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database.Commands +{ + /// + /// Command History 관리자 + /// Undo/Redo 스택 관리 및 세션 간 영속성 + /// + public class CommandHistory + { + #region Fields + private readonly Stack undoStack; + private readonly Stack redoStack; + private readonly DatabaseManager databaseManager; + + private const int MaxHistorySize = 100; // 최대 100개 명령 기록 + #endregion + + #region Properties + /// + /// Undo 가능한 명령 개수 + /// + public int UndoCount => undoStack.Count; + + /// + /// Redo 가능한 명령 개수 + /// + public int RedoCount => redoStack.Count; + + /// + /// Undo 가능 여부 + /// + public bool CanUndo => undoStack.Count > 0; + + /// + /// Redo 가능 여부 + /// + public bool CanRedo => redoStack.Count > 0; + #endregion + + #region Events + /// + /// History 변경 이벤트 (UI 업데이트용) + /// + public event Action OnHistoryChanged; + #endregion + + #region Constructor + public CommandHistory(DatabaseManager databaseManager) + { + this.databaseManager = databaseManager ?? throw new ArgumentNullException(nameof(databaseManager)); + undoStack = new Stack(); + redoStack = new Stack(); + + ToolkitLogger.LogDebug("CommandHistory", "생성 완료."); + } + #endregion + + #region Execute Command + /// + /// 명령 실행 및 히스토리 추가 + /// + public async UniTask ExecuteCommandAsync(ICommand command) + { + if (command == null) + { + throw new ArgumentNullException(nameof(command)); + } + + // 명령 실행 + bool success = await command.ExecuteAsync(); + + if (success) + { + // Undo 스택에 추가 + undoStack.Push(command); + + // Redo 스택 초기화 (새로운 명령이 실행되면 Redo 불가) + redoStack.Clear(); + + // 히스토리 크기 제한 + TrimHistory(); + + // 데이터베이스에 저장 (선택적) + if (command.CanPersist && databaseManager.IsConnected) + { + await PersistCommandAsync(command); + } + + // 이벤트 발생 + OnHistoryChanged?.Invoke(); + + ToolkitLogger.LogDebug("CommandHistory", $" 명령 실행 및 추가: {command.CommandName} (Undo: {UndoCount}, Redo: {RedoCount})"); + } + + return success; + } + + /// + /// 이미 실행된 명령을 히스토리에 기록 (실행 없이 기록만) + /// + public async UniTask RecordCommandAsync(ICommand command) + { + if (command == null) + { + throw new ArgumentNullException(nameof(command)); + } + + // Undo 스택에 추가 (이미 실행됨) + undoStack.Push(command); + + // Redo 스택 초기화 (새로운 명령이 기록되면 Redo 불가) + redoStack.Clear(); + + // 히스토리 크기 제한 + TrimHistory(); + + // 데이터베이스에 저장 (선택적) + if (command.CanPersist && databaseManager.IsConnected) + { + await PersistCommandAsync(command); + } + + // 이벤트 발생 + OnHistoryChanged?.Invoke(); + + ToolkitLogger.LogDebug("CommandHistory", $" 명령 기록: {command.CommandName} (Undo: {UndoCount}, Redo: {RedoCount})"); + } + #endregion + + #region Undo/Redo + /// + /// Undo 실행 + /// + public async UniTask UndoAsync() + { + if (!CanUndo) + { + ToolkitLogger.LogWarning("CommandHistory", "Undo 불가능 - 스택이 비어있습니다."); + return false; + } + + var command = undoStack.Pop(); + bool success = await command.UndoAsync(); + + if (success) + { + // Redo 스택에 추가 + redoStack.Push(command); + + // 이벤트 발생 + OnHistoryChanged?.Invoke(); + + ToolkitLogger.LogDebug("CommandHistory", $" Undo 완료: {command.CommandName} (Undo: {UndoCount}, Redo: {RedoCount})"); + } + else + { + // 실패 시 다시 Undo 스택에 추가 + undoStack.Push(command); + ToolkitLogger.LogError("CommandHistory", $" Undo 실패: {command.CommandName}"); + } + + return success; + } + + /// + /// Undo 실행 (동기 - WebSocket 핸들러용) + /// + /// + /// WebSocket 핸들러에서 결과를 반환받기 위한 동기 메서드입니다. + /// 내부적으로 UndoAsync()를 동기 호출합니다. + /// + public bool Undo() + { + return UndoAsync().GetAwaiter().GetResult(); + } + + /// + /// Redo 실행 + /// + public async UniTask RedoAsync() + { + if (!CanRedo) + { + ToolkitLogger.LogWarning("CommandHistory", "Redo 불가능 - 스택이 비어있습니다."); + return false; + } + + var command = redoStack.Pop(); + bool success = await command.RedoAsync(); + + if (success) + { + // Undo 스택에 추가 + undoStack.Push(command); + + // 이벤트 발생 + OnHistoryChanged?.Invoke(); + + ToolkitLogger.LogDebug("CommandHistory", $" Redo 완료: {command.CommandName} (Undo: {UndoCount}, Redo: {RedoCount})"); + } + else + { + // 실패 시 다시 Redo 스택에 추가 + redoStack.Push(command); + ToolkitLogger.LogError("CommandHistory", $" Redo 실패: {command.CommandName}"); + } + + return success; + } + + /// + /// Redo 실행 (동기 - WebSocket 핸들러용) + /// + /// + /// WebSocket 핸들러에서 결과를 반환받기 위한 동기 메서드입니다. + /// 내부적으로 RedoAsync()를 동기 호출합니다. + /// + public bool Redo() + { + return RedoAsync().GetAwaiter().GetResult(); + } + + /// + /// 다음 Undo할 명령 확인 (스택에서 제거 안함) + /// + public ICommand PeekUndo() + { + return CanUndo ? undoStack.Peek() : null; + } + + /// + /// 다음 Redo할 명령 확인 (스택에서 제거 안함) + /// + public ICommand PeekRedo() + { + return CanRedo ? redoStack.Peek() : null; + } + + /// + /// Undo 스택 목록 가져오기 + /// + public List GetUndoStack(int limit = 10) + { + var result = new List(); + var temp = new Stack(); + + int count = 0; + while (undoStack.Count > 0 && count < limit) + { + var cmd = undoStack.Pop(); + temp.Push(cmd); + result.Add(cmd); + count++; + } + + // 복원 + while (temp.Count > 0) + { + undoStack.Push(temp.Pop()); + } + + return result; + } + + /// + /// Redo 스택 목록 가져오기 + /// + public List GetRedoStack(int limit = 10) + { + var result = new List(); + var temp = new Stack(); + + int count = 0; + while (redoStack.Count > 0 && count < limit) + { + var cmd = redoStack.Pop(); + temp.Push(cmd); + result.Add(cmd); + count++; + } + + // 복원 + while (temp.Count > 0) + { + redoStack.Push(temp.Pop()); + } + + return result; + } + #endregion + + #region History Management + /// + /// 전체 히스토리 초기화 + /// + public void Clear() + { + undoStack.Clear(); + redoStack.Clear(); + OnHistoryChanged?.Invoke(); + + ToolkitLogger.LogDebug("CommandHistory", "히스토리 초기화 완료."); + } + + /// + /// 히스토리 크기 제한 + /// + private void TrimHistory() + { + if (undoStack.Count > MaxHistorySize) + { + // 성능 개선: 리스트로 변환 후 트림하고 스택 재구성 + var list = undoStack.ToList(); + list.RemoveRange(0, list.Count - MaxHistorySize); + + undoStack.Clear(); + for (int i = list.Count - 1; i >= 0; i--) + { + undoStack.Push(list[i]); + } + + ToolkitLogger.LogDebug("CommandHistory", $" 히스토리 크기 제한 적용: {undoStack.Count}개 유지"); + } + } + + /// + /// 최근 명령 목록 가져오기 (UI 표시용) + /// + public List GetRecentCommands(int count = 10) + { + var commands = new List(); + var temp = new Stack(); + + // Undo 스택에서 가져오기 + int retrievedCount = 0; + while (undoStack.Count > 0 && retrievedCount < count) + { + var cmd = undoStack.Pop(); + temp.Push(cmd); + commands.Add($"{cmd.ExecutedAt:HH:mm:ss} - {cmd.CommandName}"); + retrievedCount++; + } + + // 복원 + while (temp.Count > 0) + { + undoStack.Push(temp.Pop()); + } + + return commands; + } + #endregion + + #region Database Persistence + /// + /// 명령을 데이터베이스에 저장 + /// + private async UniTask PersistCommandAsync(ICommand command) + { + try + { + if (!databaseManager.IsConnected || databaseManager.Connector == null) + { + return; + } + + // JSON 직렬화 + string json = command.Serialize(); + + // SQL INSERT (SQLite 문법) + string sql = @" + INSERT INTO command_history ( + command_id, command_name, command_type, + command_data, executed_at, executed_by + ) + VALUES (?, ?, ?, ?, ?, ?);"; + + await UniTask.RunOnThreadPool(() => + { + var connection = databaseManager.Connector.Connection; + connection.Execute(sql, + command.CommandId, + command.CommandName, + command.GetType().Name, + json, + command.ExecutedAt.ToString("o"), // ISO 8601 format + "EditorUI" // 실행 주체 구분 + ); + }); + + ToolkitLogger.LogDebug("CommandHistory", $" 명령 저장 완료: {command.CommandName}"); + } + catch (Exception ex) + { + ToolkitLogger.LogError("CommandHistory", $" 명령 저장 실패: {ex.Message}"); + } + } + + /// + /// 데이터베이스에서 히스토리 로드 (세션 복원) + /// + public async UniTask LoadHistoryFromDatabaseAsync(DateTime since) + { + try + { + if (!databaseManager.IsConnected || databaseManager.Connector == null) + { + ToolkitLogger.LogWarning("CommandHistory", "데이터베이스 연결되지 않음 - 히스토리 로드 불가."); + return 0; + } + + // SQL SELECT (SQLite 문법) + string sql = @" + SELECT command_id, command_name, command_type, command_data, executed_at + FROM command_history + WHERE executed_at >= ? + ORDER BY executed_at ASC + LIMIT 100"; + + int loadedCount = 0; + + // DB 쿼리는 백그라운드 스레드에서 실행 + List records = null; + await UniTask.RunOnThreadPool(() => + { + var connection = databaseManager.Connector.Connection; + records = connection.Query(sql, since.ToString("o")).ToList(); + }); + + // Command 복원은 메인 스레드에서 실행 (Unity API 호출 가능) + await UniTask.SwitchToMainThread(); + + foreach (var record in records) + { + // CommandFactory를 사용하여 Command 복원 + var command = CommandFactory.CreateFromDatabase(record.command_type, record.command_data); + + if (command != null) + { + // Undo 스택에 추가 (실행 완료된 명령) + undoStack.Push(command); + loadedCount++; + + ToolkitLogger.LogDebug("CommandHistory", $" Command 복원: {command.CommandName} (Type: {record.command_type})"); + } + else + { + ToolkitLogger.LogWarning("CommandHistory", $" Command 복원 실패 - Type: {record.command_type}, ID: {record.command_id}"); + } + } + + ToolkitLogger.LogDebug("CommandHistory", $" 히스토리 로드 완료: {loadedCount}개"); + return loadedCount; + } + catch (Exception ex) + { + ToolkitLogger.LogError("CommandHistory", $" 히스토리 로드 실패: {ex.Message}"); + return 0; + } + } + + /// + /// Command History 레코드 (SQLite 쿼리 결과용) + /// + private class CommandHistoryRecord + { + 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; } + } + #endregion + + #region Status + /// + /// 히스토리 상태 정보 + /// + public HistoryStatus GetStatus() + { + return new HistoryStatus + { + UndoCount = UndoCount, + RedoCount = RedoCount, + CanUndo = CanUndo, + CanRedo = CanRedo, + MaxHistorySize = MaxHistorySize + }; + } + #endregion + } + + #region Status Struct + public struct HistoryStatus + { + public int UndoCount; + public int RedoCount; + public bool CanUndo; + public bool CanRedo; + public int MaxHistorySize; + + public override string ToString() + { + return $"[HistoryStatus] Undo: {UndoCount}, Redo: {RedoCount}, Max: {MaxHistorySize}"; + } + } + #endregion +} diff --git a/skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs.meta b/skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs.meta new file mode 100644 index 0000000..08b5885 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/CommandHistory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b79709016c210a441ad295d149fe3734 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs b/skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs new file mode 100644 index 0000000..c22f87b --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs @@ -0,0 +1,187 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database.Commands +{ + /// + /// GameObject 생성 명령 + /// + public class CreateGameObjectCommand : CommandBase + { + #region Fields + private readonly string gameObjectName; + private readonly Vector3 position; + private readonly Quaternion rotation; + private readonly int parentInstanceId; + private int createdInstanceId; + #endregion + + #region Constructor + public CreateGameObjectCommand( + string gameObjectName, + Vector3 position = default, + Quaternion rotation = default, + GameObject parent = null) + : base($"Create GameObject: {gameObjectName}") + { + this.gameObjectName = gameObjectName; + this.position = position; + this.rotation = rotation != default ? rotation : Quaternion.identity; + parentInstanceId = parent != null ? parent.GetInstanceID() : 0; + createdInstanceId = 0; + } + + /// + /// 이미 생성된 GameObject로부터 Command 생성 (중복 생성 방지) + /// + public static CreateGameObjectCommand CreateFromExisting(GameObject existingObject, GameObject parent = null) + { + if (existingObject == null) + throw new ArgumentNullException(nameof(existingObject)); + + var command = new CreateGameObjectCommand( + existingObject.name, + existingObject.transform.position, + existingObject.transform.rotation, + parent + ); + + // 이미 생성된 객체의 InstanceID 저장 + command.createdInstanceId = existingObject.GetInstanceID(); + + return command; + } + #endregion + + #region Command Implementation + protected override async UniTask OnExecuteAsync() + { + try + { + // GameObject 생성 + var go = new GameObject(gameObjectName); + go.transform.position = position; + go.transform.rotation = rotation; + + // 부모 설정 + if (parentInstanceId != 0) + { + var parent = UnityEditor.EditorUtility.InstanceIDToObject(parentInstanceId) as GameObject; + if (parent != null) + { + go.transform.SetParent(parent.transform, true); + } + } + + // 생성된 GameObject ID 저장 + createdInstanceId = go.GetInstanceID(); + + ToolkitLogger.LogDebug("CreateGameObjectCommand", $" GameObject 생성: {gameObjectName} (ID: {createdInstanceId})"); + + await UniTask.Yield(); + return true; + } + catch (Exception ex) + { + ToolkitLogger.LogError("CreateGameObjectCommand", $" 생성 실패: {ex.Message}"); + return false; + } + } + + protected override async UniTask OnUndoAsync() + { + try + { + // 생성된 GameObject 삭제 + var go = UnityEditor.EditorUtility.InstanceIDToObject(createdInstanceId) as GameObject; + if (go != null) + { + UnityEngine.Object.DestroyImmediate(go); + ToolkitLogger.LogDebug("CreateGameObjectCommand", $" GameObject 삭제 (Undo): {gameObjectName}"); + } + else + { + ToolkitLogger.LogWarning("CreateGameObjectCommand", $" GameObject를 찾을 수 없음: {gameObjectName}"); + } + + await UniTask.Yield(); + return true; + } + catch (Exception ex) + { + ToolkitLogger.LogError("CreateGameObjectCommand", $" Undo 실패: {ex.Message}"); + return false; + } + } + + protected override async UniTask OnRedoAsync() + { + // Redo는 Execute와 동일 + return await OnExecuteAsync(); + } + #endregion + + #region Serialization + public override string Serialize() + { + return JsonUtility.ToJson(new CreateGameObjectData + { + commandId = CommandId, + commandName = CommandName, + executedAt = ExecutedAt.ToString("o"), + gameObjectName = gameObjectName, + position = position, + rotation = rotation, + parentInstanceId = parentInstanceId, + createdInstanceId = createdInstanceId + }); + } + + /// + /// JSON에서 Command 복원 (세션 영속성용) + /// + public static CreateGameObjectCommand FromJson(string json) + { + var data = JsonUtility.FromJson(json); + + // 부모 GameObject 찾기 + GameObject parent = null; + if (data.parentInstanceId != 0) + { + parent = UnityEditor.EditorUtility.InstanceIDToObject(data.parentInstanceId) as GameObject; + } + + // 새 인스턴스 생성 + var command = new CreateGameObjectCommand( + data.gameObjectName, + data.position, + data.rotation, + parent + ); + + // 메타데이터 복원 + command.CommandId = data.commandId; + command.CommandName = data.commandName; + command.ExecutedAt = DateTime.Parse(data.executedAt); + command.createdInstanceId = data.createdInstanceId; + + return command; + } + + [Serializable] + private class CreateGameObjectData + { + public string commandId; + public string commandName; + public string executedAt; + public string gameObjectName; + public Vector3 position; + public Quaternion rotation; + public int parentInstanceId; + public int createdInstanceId; + } + #endregion + } +} diff --git a/skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs.meta b/skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs.meta new file mode 100644 index 0000000..de2afcb --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/CreateGameObjectCommand.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b61e4d05723fa33478874aad39fed96d \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs b/skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs new file mode 100644 index 0000000..59bb631 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs @@ -0,0 +1,156 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database.Commands +{ + /// + /// GameObject 삭제 명령 + /// 삭제 전 상태를 저장하여 Undo 지원 + /// + public class DeleteGameObjectCommand : CommandBase + { + #region Fields + private readonly int gameObjectInstanceId; + private readonly string gameObjectName; + private readonly Vector3 position; + private readonly Quaternion rotation; + private readonly Vector3 scale; + private readonly int parentInstanceId; + private readonly int siblingIndex; + private GameObject deletedGameObject; // Undo용 참조 보관 + #endregion + + #region Constructor + public DeleteGameObjectCommand(GameObject gameObject) + : base($"Delete GameObject: {gameObject.name}") + { + gameObjectInstanceId = gameObject.GetInstanceID(); + gameObjectName = gameObject.name; + position = gameObject.transform.position; + rotation = gameObject.transform.rotation; + scale = gameObject.transform.localScale; + parentInstanceId = gameObject.transform.parent != null + ? gameObject.transform.parent.gameObject.GetInstanceID() + : 0; + siblingIndex = gameObject.transform.GetSiblingIndex(); + } + #endregion + + #region Command Implementation + protected override async UniTask OnExecuteAsync() + { + try + { + var go = UnityEditor.EditorUtility.InstanceIDToObject(gameObjectInstanceId) as GameObject; + if (go == null) + { + ToolkitLogger.LogWarning("DeleteGameObjectCommand", $" GameObject를 찾을 수 없음: {gameObjectName}"); + return false; + } + + // Undo를 위해 참조 보관 (비활성화) + deletedGameObject = go; + go.SetActive(false); + go.hideFlags = HideFlags.HideInHierarchy; + + ToolkitLogger.LogDebug("DeleteGameObjectCommand", $" GameObject 삭제: {gameObjectName}"); + + await UniTask.Yield(); + return true; + } + catch (Exception ex) + { + ToolkitLogger.LogError("DeleteGameObjectCommand", $" 삭제 실패: {ex.Message}"); + return false; + } + } + + protected override async UniTask OnUndoAsync() + { + try + { + if (deletedGameObject == null) + { + ToolkitLogger.LogWarning("DeleteGameObjectCommand", $" 복원할 GameObject 참조가 없음: {gameObjectName}"); + return false; + } + + // GameObject 복원 + deletedGameObject.SetActive(true); + deletedGameObject.hideFlags = HideFlags.None; + + // 부모 및 위치 복원 + if (parentInstanceId != 0) + { + var parent = UnityEditor.EditorUtility.InstanceIDToObject(parentInstanceId) as GameObject; + if (parent != null) + { + deletedGameObject.transform.SetParent(parent.transform, false); + deletedGameObject.transform.SetSiblingIndex(siblingIndex); + } + } + + deletedGameObject.transform.position = position; + deletedGameObject.transform.rotation = rotation; + deletedGameObject.transform.localScale = scale; + + ToolkitLogger.LogDebug("DeleteGameObjectCommand", $" GameObject 복원 (Undo): {gameObjectName}"); + + await UniTask.Yield(); + return true; + } + catch (Exception ex) + { + ToolkitLogger.LogError("DeleteGameObjectCommand", $" Undo 실패: {ex.Message}"); + return false; + } + } + + protected override async UniTask OnRedoAsync() + { + // Redo는 Execute와 동일 + return await OnExecuteAsync(); + } + #endregion + + #region Persistence Override + public override bool CanPersist => false; // GameObject 참조를 포함하므로 DB 저장 불가 + #endregion + + #region Serialization + public override string Serialize() + { + return JsonUtility.ToJson(new DeleteGameObjectData + { + commandId = CommandId, + commandName = CommandName, + executedAt = ExecutedAt.ToString("o"), + gameObjectInstanceId = gameObjectInstanceId, + gameObjectName = gameObjectName, + position = position, + rotation = rotation, + scale = scale, + parentInstanceId = parentInstanceId, + siblingIndex = siblingIndex + }); + } + + [Serializable] + private class DeleteGameObjectData + { + public string commandId; + public string commandName; + public string executedAt; + public int gameObjectInstanceId; + public string gameObjectName; + public Vector3 position; + public Quaternion rotation; + public Vector3 scale; + public int parentInstanceId; + public int siblingIndex; + } + #endregion + } +} diff --git a/skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs.meta b/skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs.meta new file mode 100644 index 0000000..0a27475 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/DeleteGameObjectCommand.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 96e777a7e70d2f245be762670a226d75 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/Commands/ICommand.cs b/skills/assets/unity-package/Editor/Database/Commands/ICommand.cs new file mode 100644 index 0000000..40a41da --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/ICommand.cs @@ -0,0 +1,57 @@ +using System; +using Cysharp.Threading.Tasks; + +namespace UnityEditorToolkit.Editor.Database.Commands +{ + /// + /// Command Pattern 인터페이스 + /// 모든 실행 가능한 명령은 이 인터페이스를 구현해야 함 + /// + public interface ICommand + { + /// + /// 명령 고유 ID (데이터베이스 저장용) + /// + string CommandId { get; } + + /// + /// 명령 이름 (UI 표시용) + /// + string CommandName { get; } + + /// + /// 명령 실행 시간 + /// + DateTime ExecutedAt { get; } + + /// + /// 명령 실행 + /// + UniTask ExecuteAsync(); + + /// + /// 명령 실행 취소 (Undo) + /// + UniTask UndoAsync(); + + /// + /// 명령 재실행 (Redo) + /// + UniTask RedoAsync(); + + /// + /// 명령을 데이터베이스에 저장할 수 있는지 여부 + /// + bool CanPersist { get; } + + /// + /// 명령을 JSON으로 직렬화 + /// + string Serialize(); + + /// + /// JSON에서 명령 복원 + /// + void Deserialize(string json); + } +} diff --git a/skills/assets/unity-package/Editor/Database/Commands/ICommand.cs.meta b/skills/assets/unity-package/Editor/Database/Commands/ICommand.cs.meta new file mode 100644 index 0000000..4d6a708 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/ICommand.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 54a0273f9b663c14cb5651614952b46f \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs b/skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs new file mode 100644 index 0000000..089ec44 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs @@ -0,0 +1,160 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database.Commands +{ + /// + /// GameObject Transform 변경 명령 + /// Position, Rotation, Scale 변경을 추적하고 Undo/Redo 지원 + /// + public class TransformChangeCommand : CommandBase + { + #region Fields + private readonly int gameObjectInstanceId; + private readonly Vector3 oldPosition; + private readonly Quaternion oldRotation; + private readonly Vector3 oldScale; + private readonly Vector3 newPosition; + private readonly Quaternion newRotation; + private readonly Vector3 newScale; + private readonly string gameObjectName; + #endregion + + #region Constructor + public TransformChangeCommand( + GameObject gameObject, + Vector3 oldPosition, Quaternion oldRotation, Vector3 oldScale, + Vector3 newPosition, Quaternion newRotation, Vector3 newScale) + : base($"Transform Change: {gameObject.name}") + { + gameObjectInstanceId = gameObject.GetInstanceID(); + gameObjectName = gameObject.name; + + this.oldPosition = oldPosition; + this.oldRotation = oldRotation; + this.oldScale = oldScale; + + this.newPosition = newPosition; + this.newRotation = newRotation; + this.newScale = newScale; + } + #endregion + + #region Command Implementation + protected override async UniTask OnExecuteAsync() + { + var go = GetGameObject(); + if (go == null) return false; + + // 새로운 Transform 적용 + go.transform.position = newPosition; + go.transform.rotation = newRotation; + go.transform.localScale = newScale; + + await UniTask.Yield(); + return true; + } + + protected override async UniTask OnUndoAsync() + { + var go = GetGameObject(); + if (go == null) return false; + + // 이전 Transform 복원 + go.transform.position = oldPosition; + go.transform.rotation = oldRotation; + go.transform.localScale = oldScale; + + await UniTask.Yield(); + return true; + } + + protected override async UniTask OnRedoAsync() + { + // Redo는 Execute와 동일 + return await OnExecuteAsync(); + } + #endregion + + #region Helper Methods + private GameObject GetGameObject() + { + var go = UnityEditor.EditorUtility.InstanceIDToObject(gameObjectInstanceId) as GameObject; + if (go == null) + { + ToolkitLogger.LogWarning("TransformChangeCommand", $"GameObject not found: {gameObjectName} (ID: {gameObjectInstanceId})"); + } + return go; + } + #endregion + + #region Serialization + public override string Serialize() + { + return JsonUtility.ToJson(new TransformChangeData + { + commandId = CommandId, + commandName = CommandName, + executedAt = ExecutedAt.ToString("o"), + gameObjectInstanceId = gameObjectInstanceId, + gameObjectName = gameObjectName, + oldPosition = oldPosition, + oldRotation = oldRotation, + oldScale = oldScale, + newPosition = newPosition, + newRotation = newRotation, + newScale = newScale + }); + } + + /// + /// JSON에서 Command 복원 (세션 영속성용) + /// + public static TransformChangeCommand FromJson(string json) + { + var data = JsonUtility.FromJson(json); + + // GameObject 찾기 + var go = UnityEditor.EditorUtility.InstanceIDToObject(data.gameObjectInstanceId) as GameObject; + if (go == null) + { + ToolkitLogger.LogWarning("TransformChangeCommand", $"GameObject not found (FromJson): {data.gameObjectName} (ID: {data.gameObjectInstanceId})"); + // GameObject가 없어도 Command는 생성 (히스토리 기록용) + go = new GameObject(data.gameObjectName); + } + + // 새 인스턴스 생성 + var command = new TransformChangeCommand( + go, + data.oldPosition, data.oldRotation, data.oldScale, + data.newPosition, data.newRotation, data.newScale + ); + + // 메타데이터 복원 + command.CommandId = data.commandId; + command.CommandName = data.commandName; + command.ExecutedAt = DateTime.Parse(data.executedAt); + + return command; + } + + [Serializable] + private class TransformChangeData + { + public string commandId; + public string commandName; + public string executedAt; + public int gameObjectInstanceId; + public string gameObjectName; + public Vector3 oldPosition; + public Quaternion oldRotation; + public Vector3 oldScale; + public Vector3 newPosition; + public Quaternion newRotation; + public Vector3 newScale; + } + #endregion + } +} diff --git a/skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs.meta b/skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs.meta new file mode 100644 index 0000000..874f5c5 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Commands/TransformChangeCommand.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8a0112613fb1ea24bb6632ee8960a2f2 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/DatabaseConfig.cs b/skills/assets/unity-package/Editor/Database/DatabaseConfig.cs new file mode 100644 index 0000000..768aea3 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/DatabaseConfig.cs @@ -0,0 +1,347 @@ +using System; +using System.IO; +using UnityEngine; +using UnityEditor; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database +{ + /// + /// SQLite 데이터베이스 연결 설정 + /// 임베디드 SQLite - 설치 불필요, 단일 파일 DB + /// EditorPrefs에 개별 키로 저장/로드 + /// + [Serializable] + public class DatabaseConfig + { + #region EditorPrefs Keys + /// + /// EditorPrefs 키 상수 + /// + private const string PREF_KEY_CONFIG_VERSION = "UnityEditorToolkit.Database.ConfigVersion"; + private const string PREF_KEY_ENABLE_DATABASE = "UnityEditorToolkit.Database.EnableDatabase"; + private const string PREF_KEY_FILE_PATH = "UnityEditorToolkit.Database.FilePath"; + private const string PREF_KEY_ENABLE_WAL = "UnityEditorToolkit.Database.EnableWAL"; + private const string PREF_KEY_ENABLE_ENCRYPTION = "UnityEditorToolkit.Database.EnableEncryption"; + private const string PREF_KEY_ENCRYPTION_KEY = "UnityEditorToolkit.Database.EncryptionKey"; + + /// + /// 현재 설정 버전 + /// + public const int CURRENT_VERSION = 1; + #endregion + + #region Private Fields + /// + /// 설정 버전 (마이그레이션용) + /// v1: EnableWAL 기본값 true 변경, EnableDatabase 기본값 true 변경 + /// + [SerializeField] + private int configVersion = 0; + + /// + /// 데이터베이스 기능 활성화 여부 + /// + [SerializeField] + private bool enableDatabase = true; + + /// + /// SQLite 데이터베이스 파일 경로 + /// 기본값: Application.persistentDataPath + "/unity_editor_toolkit.db" + /// + [SerializeField] + private string databaseFilePath = ""; + + /// + /// WAL (Write-Ahead Logging) 모드 사용 여부 + /// 성능 향상 및 동시성 개선 + /// + [SerializeField] + private bool enableWAL = true; + + /// + /// 암호화 사용 여부 (SQLite Multiple Ciphers) + /// + [SerializeField] + private bool enableEncryption = false; + + /// + /// 암호화 키 (암호화 사용 시 필요) + /// 주의: 평문 저장, 프로덕션에서는 보안 저장소 사용 권장 + /// + [SerializeField] + private string encryptionKey = ""; + #endregion + + #region Public Properties + public int ConfigVersion + { + get => configVersion; + set => configVersion = value; + } + + public bool EnableDatabase + { + get => enableDatabase; + set => enableDatabase = value; + } + + public string DatabaseFilePath + { + get + { + // 빈 문자열이면 기본 경로 반환 + if (string.IsNullOrEmpty(databaseFilePath)) + { + return GetDefaultDatabasePath(); + } + return databaseFilePath; + } + set => databaseFilePath = value; + } + + public bool EnableWAL + { + get => enableWAL; + set => enableWAL = value; + } + + public bool EnableEncryption + { + get => enableEncryption; + set => enableEncryption = value; + } + + public string EncryptionKey + { + get => encryptionKey; + set => encryptionKey = value; + } + #endregion + + #region Connection String + /// + /// SQLite 연결 문자열 생성 + /// + /// SQLite 연결 문자열 + public string GetConnectionString() + { + string dbPath = DatabaseFilePath; + + // 기본 연결 문자열 + string connectionString = $"Data Source={dbPath}"; + + // 암호화 사용 시 + if (enableEncryption && !string.IsNullOrEmpty(encryptionKey)) + { + connectionString += $";Password={encryptionKey}"; + } + + return connectionString; + } + #endregion + + #region Static Methods + /// + /// 기본 데이터베이스 파일 경로 가져오기 + /// + /// 기본 경로 + public static string GetDefaultDatabasePath() + { + // Unity persistentDataPath 사용 (플랫폼별 자동 선택) + // Windows: %USERPROFILE%\AppData\LocalLow\CompanyName\ProductName + // macOS: ~/Library/Application Support/CompanyName/ProductName + // Linux: ~/.config/unity3d/CompanyName/ProductName + return Path.Combine(Application.persistentDataPath, "unity_editor_toolkit.db"); + } + #endregion + + #region Validation + /// + /// 설정 유효성 검증 + /// + /// 유효성 검증 결과 + public ValidationResult Validate() + { + // 데이터베이스 비활성화 시 검증 통과 + if (!enableDatabase) + { + return new ValidationResult { IsValid = true }; + } + + // 파일 경로 검증 + string dbPath = DatabaseFilePath; + if (string.IsNullOrWhiteSpace(dbPath)) + { + return new ValidationResult + { + IsValid = false, + ErrorMessage = "Database file path는 필수 항목입니다." + }; + } + + // 디렉토리 존재 여부 확인 (없으면 생성 가능하도록 경고만) + string directory = Path.GetDirectoryName(dbPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + try + { + Directory.CreateDirectory(directory); + } + catch (Exception ex) + { + return new ValidationResult + { + IsValid = false, + ErrorMessage = $"디렉토리 생성 실패: {ex.Message}" + }; + } + } + + // 암호화 사용 시 키 검증 + if (enableEncryption && string.IsNullOrWhiteSpace(encryptionKey)) + { + return new ValidationResult + { + IsValid = false, + ErrorMessage = "암호화 사용 시 Encryption Key는 필수 항목입니다." + }; + } + + return new ValidationResult { IsValid = true }; + } + #endregion + + #region Reset + /// + /// 기본값으로 초기화 + /// + public void Reset() + { + configVersion = CURRENT_VERSION; + enableDatabase = true; // SQLite는 설치 불필요, 기본 활성화 + databaseFilePath = ""; + enableWAL = true; + enableEncryption = false; + encryptionKey = ""; + } + #endregion + + #region EditorPrefs Save/Load + /// + /// EditorPrefs에 개별 키로 저장 + /// + public void SaveToEditorPrefs() + { + EditorPrefs.SetInt(PREF_KEY_CONFIG_VERSION, configVersion); + EditorPrefs.SetBool(PREF_KEY_ENABLE_DATABASE, enableDatabase); + EditorPrefs.SetString(PREF_KEY_FILE_PATH, databaseFilePath); + EditorPrefs.SetBool(PREF_KEY_ENABLE_WAL, enableWAL); + EditorPrefs.SetBool(PREF_KEY_ENABLE_ENCRYPTION, enableEncryption); + EditorPrefs.SetString(PREF_KEY_ENCRYPTION_KEY, encryptionKey); + } + + /// + /// EditorPrefs에서 개별 키로 로드 (마이그레이션 포함) + /// + /// DatabaseConfig 인스턴스 + public static DatabaseConfig LoadFromEditorPrefs() + { + var config = new DatabaseConfig(); + + // ConfigVersion 확인 (마이그레이션 판단용) + int savedVersion = EditorPrefs.GetInt(PREF_KEY_CONFIG_VERSION, -1); + + if (savedVersion == -1) + { + // 첫 실행: 기본값 사용 (저장하지 않음) + ToolkitLogger.Log("DatabaseConfig", "첫 실행 감지 - 기본값 사용"); + config.Reset(); + return config; + } + + // 개별 키에서 로드 + config.configVersion = savedVersion; + config.enableDatabase = EditorPrefs.GetBool(PREF_KEY_ENABLE_DATABASE, true); + config.databaseFilePath = EditorPrefs.GetString(PREF_KEY_FILE_PATH, ""); + config.enableWAL = EditorPrefs.GetBool(PREF_KEY_ENABLE_WAL, true); + config.enableEncryption = EditorPrefs.GetBool(PREF_KEY_ENABLE_ENCRYPTION, false); + config.encryptionKey = EditorPrefs.GetString(PREF_KEY_ENCRYPTION_KEY, ""); + + // 마이그레이션 필요 여부 확인 + if (config.configVersion < CURRENT_VERSION) + { + ToolkitLogger.Log("DatabaseConfig", $"마이그레이션: v{config.configVersion} → v{CURRENT_VERSION}"); + MigrateConfig(config); + config.SaveToEditorPrefs(); // 마이그레이션 후 저장 + } + + return config; + } + + /// + /// EditorPrefs 키 모두 삭제 + /// + public static void ClearEditorPrefs() + { + EditorPrefs.DeleteKey(PREF_KEY_CONFIG_VERSION); + EditorPrefs.DeleteKey(PREF_KEY_ENABLE_DATABASE); + EditorPrefs.DeleteKey(PREF_KEY_FILE_PATH); + EditorPrefs.DeleteKey(PREF_KEY_ENABLE_WAL); + EditorPrefs.DeleteKey(PREF_KEY_ENABLE_ENCRYPTION); + EditorPrefs.DeleteKey(PREF_KEY_ENCRYPTION_KEY); + ToolkitLogger.Log("DatabaseConfig", "EditorPrefs 초기화 완료"); + } + + /// + /// 설정 마이그레이션 (이전 버전 → 최신 버전) + /// + /// 마이그레이션할 설정 + private static void MigrateConfig(DatabaseConfig config) + { + // v0 → v1: EnableWAL과 EnableDatabase 기본값을 true로 변경 + if (config.configVersion < 1) + { + bool changed = false; + + if (!config.enableWAL) + { + ToolkitLogger.Log("DatabaseConfig", "마이그레이션: EnableWAL을 true로 변경 (v0 → v1)"); + config.enableWAL = true; + changed = true; + } + + if (!config.enableDatabase) + { + ToolkitLogger.Log("DatabaseConfig", "마이그레이션: EnableDatabase를 true로 변경 (v0 → v1)"); + config.enableDatabase = true; + changed = true; + } + + if (changed) + { + ToolkitLogger.Log("DatabaseConfig", "SQLite는 설치가 필요없으므로 Database 기능이 기본 활성화됩니다."); + } + + config.configVersion = 1; + } + } + #endregion + } + + /// + /// 설정 유효성 검증 결과 + /// + public struct ValidationResult + { + /// + /// 유효성 검증 통과 여부 + /// + public bool IsValid; + + /// + /// 검증 실패 시 에러 메시지 + /// + public string ErrorMessage; + } +} diff --git a/skills/assets/unity-package/Editor/Database/DatabaseConfig.cs.meta b/skills/assets/unity-package/Editor/Database/DatabaseConfig.cs.meta new file mode 100644 index 0000000..5304b1e --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/DatabaseConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8f131f462b224eb479a0d27ce5c885b5 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/DatabaseManager.cs b/skills/assets/unity-package/Editor/Database/DatabaseManager.cs new file mode 100644 index 0000000..f07fd4b --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/DatabaseManager.cs @@ -0,0 +1,598 @@ +using System; +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEditor; +using UnityEditor.Compilation; +using UnityEditorToolkit.Editor.Database.Commands; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database +{ + /// + /// SQLite 데이터베이스 관리 싱글톤 + /// 임베디드 SQLite - 설치 불필요, 단일 파일 DB + /// Domain Reload 시 자동으로 연결 정리 및 재연결 + /// + [InitializeOnLoad] + public class DatabaseManager + { + #region Domain Reload Handling + private const string PREF_KEY_DB_WAS_CONNECTED = "UnityEditorToolkit.Database.WasConnected"; + private const string PREF_KEY_DB_PATH = "UnityEditorToolkit.Database.Path"; + private const string PREF_KEY_DB_ENABLE_WAL = "UnityEditorToolkit.Database.EnableWAL"; + + static DatabaseManager() + { + // Domain Reload 전: 연결 정리 + AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; + + // Domain Reload 후: 자동 재연결 + EditorApplication.delayCall += OnAfterAssemblyReload; + } + + private static void OnBeforeAssemblyReload() + { + if (instance != null && instance.IsConnected) + { + ToolkitLogger.Log("DatabaseManager", "Domain Reload 감지 - 연결 상태 저장 및 정리 중..."); + + // 연결 상태 저장 + EditorPrefs.SetBool(PREF_KEY_DB_WAS_CONNECTED, true); + if (instance.config != null) + { + EditorPrefs.SetString(PREF_KEY_DB_PATH, instance.config.DatabaseFilePath); + EditorPrefs.SetBool(PREF_KEY_DB_ENABLE_WAL, instance.config.EnableWAL); + } + + // 연결 정리 (동기 방식) + try + { + instance.connector?.DisconnectAsync().Forget(); + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseManager", $" Shutdown 중 예외: {ex.Message}"); + } + } + } + + private static void OnAfterAssemblyReload() + { + // 이전에 연결되어 있었는지 확인 + bool wasConnected = EditorPrefs.GetBool(PREF_KEY_DB_WAS_CONNECTED, false); + + if (wasConnected) + { + ToolkitLogger.Log("DatabaseManager", "Domain Reload 완료 - 자동 재연결 시도..."); + + // 연결 상태 플래그 클리어 + EditorPrefs.DeleteKey(PREF_KEY_DB_WAS_CONNECTED); + + // 설정 복원 및 재연결 + string dbPath = EditorPrefs.GetString(PREF_KEY_DB_PATH, ""); + bool enableWAL = EditorPrefs.GetBool(PREF_KEY_DB_ENABLE_WAL, true); + + if (!string.IsNullOrEmpty(dbPath)) + { + var config = new DatabaseConfig + { + DatabaseFilePath = dbPath, + EnableWAL = enableWAL + }; + + // 비동기 재연결 + Instance.InitializeAsync(config).Forget(); + ToolkitLogger.Log("DatabaseManager", "자동 재연결 완료."); + } + } + } + #endregion + + #region Singleton + private static DatabaseManager instance; + private static readonly object @lock = new object(); + + public static DatabaseManager Instance + { + get + { + if (instance == null) + { + lock (@lock) + { + if (instance == null) + { + instance = new DatabaseManager(); + } + } + } + return instance; + } + } + + private DatabaseManager() + { + // Private constructor for singleton + } + #endregion + + #region Fields + private DatabaseConfig config; + private SQLiteConnector connector; + private CommandHistory commandHistory; + private SyncManager syncManager; + private bool isInitialized = false; + private bool isConnected = false; + private CancellationTokenSource lifecycleCts; + private static bool isInitializing = false; // Race condition prevention + private static UniTaskCompletionSource initializationTcs; // Wait-based: 초기화 결과 공유 + private bool isMigrationRunning = false; // Migration in progress flag + #endregion + + #region Properties + /// + /// 데이터베이스 초기화 완료 여부 + /// + public bool IsInitialized => isInitialized; + + /// + /// 데이터베이스 연결 상태 + /// + public bool IsConnected => isConnected && connector != null && connector.IsConnected; + + /// + /// 현재 데이터베이스 설정 + /// + public DatabaseConfig Config => config; + + /// + /// SQLite 커넥터 + /// + public SQLiteConnector Connector => connector; + + /// + /// Command History (Undo/Redo) + /// + public CommandHistory CommandHistory => commandHistory; + + /// + /// Sync Manager (실시간 동기화) + /// + public SyncManager SyncManager => syncManager; + + /// + /// 마이그레이션 실행 중 여부 + /// + public bool IsMigrationRunning => isMigrationRunning; + #endregion + + #region Initialization + /// + /// 데이터베이스 초기화 + /// + /// 데이터베이스 설정 + public async UniTask InitializeAsync(DatabaseConfig config) + { + // Wait-based: 이미 진행 중인 초기화가 있으면 완료 대기 + UniTaskCompletionSource existingTcs = null; + + // Race condition prevention: 이미 초기화 진행 중인 경우 + lock (@lock) + { + if (isInitializing) + { + // 진행 중인 초기화가 있으면 TCS 참조 저장 (lock 밖에서 대기) + existingTcs = initializationTcs; + } + else if (isInitialized) + { + // 이미 초기화된 경우 + ToolkitLogger.LogWarning("DatabaseManager", "이미 초기화되었습니다. Shutdown 후 재초기화하세요."); + return new InitializationResult + { + Success = false, + ErrorMessage = "Already initialized. Call Shutdown() first." + }; + } + else + { + // 새로운 초기화 시작 + isInitializing = true; + initializationTcs = new UniTaskCompletionSource(); + } + } + + // 진행 중인 초기화가 있으면 완료될 때까지 대기 후 결과 공유 + if (existingTcs != null) + { + ToolkitLogger.Log("DatabaseManager", "다른 초기화가 진행 중입니다. 완료를 대기합니다..."); + return await existingTcs.Task; + } + + // 새로운 초기화 시작 + InitializationResult result = default; + + try + { + // 데이터베이스 비활성화 시 + if (!config.EnableDatabase) + { + ToolkitLogger.Log("DatabaseManager", "데이터베이스 기능이 비활성화되어 있습니다."); + result = new InitializationResult + { + Success = true, + Message = "Database feature is disabled." + }; + return result; + } + + // 설정 유효성 검증 + var validation = config.Validate(); + if (!validation.IsValid) + { + ToolkitLogger.LogError("DatabaseManager", $" 설정 유효성 검증 실패: {validation.ErrorMessage}"); + result = new InitializationResult + { + Success = false, + ErrorMessage = validation.ErrorMessage + }; + return result; + } + + // 설정 저장 + this.config = config; + + // CancellationTokenSource 생성 + lifecycleCts = new CancellationTokenSource(); + + // SQLite 커넥터 생성 + connector = new SQLiteConnector(this.config); + + // Command History 생성 + commandHistory = new CommandHistory(this); + + // 연결 테스트 + var connectResult = await connector.ConnectAsync(lifecycleCts.Token); + if (!connectResult.Success) + { + ToolkitLogger.LogError("DatabaseManager", $" 연결 실패: {connectResult.ErrorMessage}"); + await CleanupAsync(); + result = new InitializationResult + { + Success = false, + ErrorMessage = connectResult.ErrorMessage + }; + return result; + } + + isConnected = true; + isInitialized = true; + + // 자동 마이그레이션 실행 + await RunAutoMigrationAsync(lifecycleCts.Token); + + // SyncManager 초기화 + syncManager = new SyncManager(this); + ToolkitLogger.Log("DatabaseManager", "SyncManager initialized."); + + ToolkitLogger.Log("DatabaseManager", $" 초기화 완료: {this.config.DatabaseFilePath}"); + result = new InitializationResult + { + Success = true, + Message = "Initialization successful." + }; + return result; + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseManager", $" 초기화 중 예외 발생: {ex.Message}\n{ex.StackTrace}"); + await CleanupAsync(); + result = new InitializationResult + { + Success = false, + ErrorMessage = ex.Message + }; + return result; + } + finally + { + // TCS에 결과 설정하여 대기 중인 호출자들에게 결과 전달 + lock (@lock) + { + isInitializing = false; + var tcs = initializationTcs; + initializationTcs = null; + tcs?.TrySetResult(result); + } + } + } + + /// + /// 데이터베이스 종료 및 리소스 정리 + /// + public async UniTask ShutdownAsync() + { + if (!isInitialized) + { + return; + } + + ToolkitLogger.Log("DatabaseManager", "Shutting down..."); + + try + { + // CancellationToken 취소 + lifecycleCts?.Cancel(); + + // 리소스 정리 + await CleanupAsync(); + + isInitialized = false; + isConnected = false; + + ToolkitLogger.Log("DatabaseManager", "Shutdown 완료."); + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseManager", $" Shutdown 중 예외 발생: {ex.Message}"); + } + } + + /// + /// 내부 리소스 정리 + /// + private async UniTask CleanupAsync() + { + try + { + // SyncManager 정리 + if (syncManager != null) + { + syncManager.Dispose(); + syncManager = null; + ToolkitLogger.Log("DatabaseManager", "SyncManager disposed."); + } + + // Command History 정리 + if (commandHistory != null) + { + commandHistory.Clear(); + commandHistory = null; + } + + // 커넥터 정리 + if (connector != null) + { + await connector.DisconnectAsync(); + connector = null; + } + + // CancellationTokenSource 정리 + lifecycleCts?.Dispose(); + lifecycleCts = null; + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseManager", $" Cleanup 중 예외 발생: {ex.Message}"); + } + } + #endregion + + #region Connection Management + /// + /// 데이터베이스 연결 해제 (동기 방식 - 서버 종료/Assembly Reload 시 사용) + /// + public void Disconnect() + { + if (!isInitialized || !isConnected) + { + return; + } + + ToolkitLogger.Log("DatabaseManager", "서버 종료로 인한 연결 해제..."); + + try + { + // CancellationToken 취소 + lifecycleCts?.Cancel(); + + // SyncManager 정리 + if (syncManager != null) + { + syncManager.Dispose(); + syncManager = null; + } + + // Command History 정리 + if (commandHistory != null) + { + commandHistory.Clear(); + commandHistory = null; + } + + // 커넥터 정리 (동기 방식) + connector?.Disconnect(); + connector = null; + + // CancellationTokenSource 정리 + lifecycleCts?.Dispose(); + lifecycleCts = null; + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseManager", $" Disconnect 중 예외: {ex.Message}"); + } + finally + { + // 상태 업데이트 + isConnected = false; + isInitialized = false; + } + + ToolkitLogger.Log("DatabaseManager", "연결 해제 완료."); + } + + /// + /// 연결 상태 확인 + /// + public async UniTask TestConnectionAsync() + { + if (!isInitialized || connector == null) + { + return false; + } + + try + { + return await connector.TestConnectionAsync(lifecycleCts?.Token ?? default); + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseManager", $" 연결 테스트 실패: {ex.Message}"); + return false; + } + } + + /// + /// 연결 재시도 + /// + public async UniTask ReconnectAsync() + { + if (!isInitialized || config == null) + { + ToolkitLogger.LogWarning("DatabaseManager", "초기화되지 않았습니다."); + return false; + } + + try + { + ToolkitLogger.Log("DatabaseManager", "재연결 시도 중..."); + + // 기존 연결 종료 + if (connector != null) + { + await connector.DisconnectAsync(); + } + + // 새 커넥터 생성 + connector = new SQLiteConnector(config); + + // 연결 + var result = await connector.ConnectAsync(lifecycleCts?.Token ?? default); + isConnected = result.Success; + + if (isConnected) + { + ToolkitLogger.Log("DatabaseManager", "재연결 성공."); + } + else + { + ToolkitLogger.LogError("DatabaseManager", $" 재연결 실패: {result.ErrorMessage}"); + } + + return isConnected; + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseManager", $" 재연결 중 예외 발생: {ex.Message}"); + isConnected = false; + return false; + } + } + #endregion + + #region Auto Migration + /// + /// 자동 마이그레이션 실행 (연결 시 자동 호출) + /// + private async UniTask RunAutoMigrationAsync(CancellationToken cancellationToken) + { + isMigrationRunning = true; + try + { + ToolkitLogger.Log("DatabaseManager", "자동 마이그레이션 확인 중..."); + + var migrationRunner = new MigrationRunner(this); + var result = await migrationRunner.RunMigrationsAsync(cancellationToken); + + if (result.Success) + { + if (result.MigrationsApplied > 0) + { + ToolkitLogger.Log("DatabaseManager", $" 자동 마이그레이션 완료: {result.MigrationsApplied}개 적용됨"); + } + else + { + ToolkitLogger.Log("DatabaseManager", "마이그레이션이 최신 상태입니다."); + } + } + else + { + ToolkitLogger.LogWarning("DatabaseManager", $" 자동 마이그레이션 실패: {result.ErrorMessage}"); + } + } + catch (Exception ex) + { + ToolkitLogger.LogWarning("DatabaseManager", $" 자동 마이그레이션 중 예외 발생: {ex.Message}"); + // 마이그레이션 실패해도 연결은 유지 + } + finally + { + isMigrationRunning = false; + } + } + #endregion + + #region Health Check + /// + /// 데이터베이스 상태 정보 조회 + /// + public DatabaseHealthStatus GetHealthStatus() + { + return new DatabaseHealthStatus + { + IsInitialized = isInitialized, + IsConnected = IsConnected, + IsEnabled = config?.EnableDatabase ?? false, + DatabaseFilePath = config?.DatabaseFilePath ?? "N/A", + DatabaseFileExists = connector?.DatabaseFileExists() ?? false + }; + } + #endregion + } + + #region Result Structs + /// + /// 초기화 결과 + /// + public struct InitializationResult + { + public bool Success; + public string Message; + public string ErrorMessage; + } + + /// + /// 데이터베이스 상태 + /// + public struct DatabaseHealthStatus + { + public bool IsInitialized; + public bool IsConnected; + public bool IsEnabled; + public string DatabaseFilePath; + public bool DatabaseFileExists; + + public override string ToString() + { + return $"[DatabaseHealthStatus]\n" + + $" Initialized: {IsInitialized}\n" + + $" Connected: {IsConnected}\n" + + $" Enabled: {IsEnabled}\n" + + $" Database File: {DatabaseFilePath}\n" + + $" File Exists: {DatabaseFileExists}"; + } + } + #endregion +} diff --git a/skills/assets/unity-package/Editor/Database/DatabaseManager.cs.meta b/skills/assets/unity-package/Editor/Database/DatabaseManager.cs.meta new file mode 100644 index 0000000..78eca73 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/DatabaseManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3c63a562fc80ebc42a10afeb117fa4a2 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/MigrationRunner.cs b/skills/assets/unity-package/Editor/Database/MigrationRunner.cs new file mode 100644 index 0000000..848ab75 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/MigrationRunner.cs @@ -0,0 +1,596 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database +{ + /// + /// 데이터베이스 마이그레이션 자동 실행 + /// SQL 파일을 순서대로 실행하여 스키마 버전 관리 + /// SQLite 버전 - 트랜잭션 지원 + /// + public class MigrationRunner + { + #region Fields + private readonly DatabaseManager databaseManager; + private readonly string migrationsPath; + #endregion + + #region Constructor + public MigrationRunner(DatabaseManager databaseManager, string migrationsPath = null) + { + this.databaseManager = databaseManager ?? throw new ArgumentNullException(nameof(databaseManager)); + + // 마이그레이션 폴더 경로 (기본값: Editor/Database/Migrations) + if (string.IsNullOrEmpty(migrationsPath)) + { + // Unity 패키지 내 Migrations 폴더 경로 + this.migrationsPath = Path.Combine(Application.dataPath, "..", "Packages", + "com.devgom.unity-editor-toolkit", "Editor", "Database", "Migrations"); + } + else + { + this.migrationsPath = migrationsPath; + } + + ToolkitLogger.LogDebug("MigrationRunner", $" 생성 완료. Migrations Path: {this.migrationsPath}"); + } + #endregion + + #region Migration Execution + /// + /// 모든 마이그레이션 실행 (순서대로) + /// + public async UniTask RunMigrationsAsync(CancellationToken cancellationToken = default) + { + if (!databaseManager.IsInitialized || !databaseManager.IsConnected) + { + return new MigrationResult + { + Success = false, + ErrorMessage = "DatabaseManager not initialized or not connected." + }; + } + + try + { + ToolkitLogger.LogDebug("MigrationRunner", "마이그레이션 시작..."); + + // 1. migrations 테이블 생성 (존재하지 않으면) + await EnsureMigrationTableExistsAsync(cancellationToken); + + // 2. 실행된 마이그레이션 목록 조회 + var appliedMigrations = await GetAppliedMigrationsAsync(cancellationToken); + ToolkitLogger.LogDebug("MigrationRunner", $" 이미 실행된 마이그레이션: {appliedMigrations.Count}개"); + + // 3. 마이그레이션 파일 목록 조회 + var migrationFiles = GetMigrationFiles(); + if (migrationFiles.Count == 0) + { + ToolkitLogger.LogWarning("MigrationRunner", $" 마이그레이션 파일이 없습니다: {migrationsPath}"); + return new MigrationResult + { + Success = true, + Message = "No migration files found.", + MigrationsApplied = 0 + }; + } + + ToolkitLogger.LogDebug("MigrationRunner", $" 발견된 마이그레이션 파일: {migrationFiles.Count}개"); + + // 4. 미실행 마이그레이션 필터링 + var pendingMigrations = migrationFiles + .Where(file => !appliedMigrations.Contains(Path.GetFileNameWithoutExtension(file))) + .OrderBy(file => file) + .ToList(); + + if (pendingMigrations.Count == 0) + { + ToolkitLogger.LogDebug("MigrationRunner", "실행할 마이그레이션이 없습니다."); + return new MigrationResult + { + Success = true, + Message = "All migrations already applied.", + MigrationsApplied = 0 + }; + } + + ToolkitLogger.LogDebug("MigrationRunner", $" 실행할 마이그레이션: {pendingMigrations.Count}개"); + + // 5. 마이그레이션 실행 + int appliedCount = 0; + foreach (var migrationFile in pendingMigrations) + { + string migrationName = Path.GetFileNameWithoutExtension(migrationFile); + ToolkitLogger.LogDebug("MigrationRunner", $" 실행 중: {migrationName}"); + + var result = await ApplyMigrationAsync(migrationFile, cancellationToken); + if (!result.Success) + { + ToolkitLogger.LogError("MigrationRunner", $" 마이그레이션 실패: {migrationName}\n{result.ErrorMessage}"); + return new MigrationResult + { + Success = false, + ErrorMessage = $"Failed to apply migration: {migrationName}\n{result.ErrorMessage}", + MigrationsApplied = appliedCount + }; + } + + appliedCount++; + ToolkitLogger.LogDebug("MigrationRunner", $" 완료: {migrationName}"); + } + + ToolkitLogger.LogDebug("MigrationRunner", $" 마이그레이션 완료: {appliedCount}개 적용됨"); + return new MigrationResult + { + Success = true, + Message = $"Successfully applied {appliedCount} migration(s).", + MigrationsApplied = appliedCount + }; + } + catch (Exception ex) + { + ToolkitLogger.LogError("MigrationRunner", $" 마이그레이션 중 예외 발생: {ex.Message}\n{ex.StackTrace}"); + return new MigrationResult + { + Success = false, + ErrorMessage = ex.Message, + MigrationsApplied = 0 + }; + } + } + + /// + /// 단일 마이그레이션 실행 + /// + private async UniTask ApplyMigrationAsync(string filePath, CancellationToken cancellationToken) + { + try + { + // SQL 파일 읽기 + string sql = File.ReadAllText(filePath); + if (string.IsNullOrWhiteSpace(sql)) + { + return new MigrationResult + { + Success = false, + ErrorMessage = "Migration file is empty." + }; + } + + string migrationName = Path.GetFileNameWithoutExtension(filePath); + + await UniTask.RunOnThreadPool(() => + { + var connection = databaseManager.Connector.Connection; + + // 트랜잭션으로 마이그레이션 원자성 보장 + // 마이그레이션 중 오류 발생 시 롤백으로 데이터베이스 일관성 유지 + connection.BeginTransaction(); + try + { + // SQL을 완전한 문장 단위로 분리 (BEGIN...END 블록 고려) + var sqlStatements = SplitSqlStatements(sql); + int executedCount = 0; + + foreach (var statement in sqlStatements) + { + var trimmedStatement = statement.Trim(); + if (string.IsNullOrWhiteSpace(trimmedStatement)) + continue; + + // 주석만 있는 문장 스킵 (Execute()에 전달하면 "not an error" 발생) + string withoutComments = RemoveSqlComments(trimmedStatement); + if (string.IsNullOrWhiteSpace(withoutComments)) + { + ToolkitLogger.LogDebug("MigrationRunner", $" 주석만 있는 문장 스킵 (RemoveSqlComments v2 적용됨)"); + continue; + } + + // 주석이 제거된 SQL 사용 (Execute()가 주석을 처리하지 못할 수 있음) + string cleanedSql = withoutComments.Trim(); + + // SELECT 문 (결과 메시지용)은 스킵 - Execute()는 결과를 반환하지 않음 + if (cleanedSql.StartsWith("SELECT ", StringComparison.OrdinalIgnoreCase)) + { + ToolkitLogger.LogDebug("MigrationRunner", $" SELECT 문 스킵"); + continue; + } + + // PRAGMA 문은 스킵 (SQLiteConnector에서 이미 설정됨) + if (cleanedSql.StartsWith("PRAGMA ", StringComparison.OrdinalIgnoreCase)) + { + ToolkitLogger.LogDebug("MigrationRunner", $" PRAGMA 문 스킵: {cleanedSql}"); + continue; + } + + // SQL 실행 (오류 발생 시 어떤 문에서 발생했는지 확인용) + try + { + connection.Execute(cleanedSql); + executedCount++; + } + catch (Exception sqlEx) + { + // SQL 문의 첫 100자만 출력 (너무 길면 로그가 지저분해짐) + string sqlPreview = trimmedStatement.Length > 100 + ? trimmedStatement.Substring(0, 100) + "..." + : trimmedStatement; + ToolkitLogger.LogError("MigrationRunner", $" SQL 실행 실패 ({executedCount + 1}번째): {sqlEx.Message}\nSQL: {sqlPreview}"); + throw; + } + } + + ToolkitLogger.LogDebug("MigrationRunner", $" SQL 문장 실행 완료: {executedCount}개"); + + // migrations 테이블에 기록 + string insertSql = @" + INSERT INTO migrations (migration_name, applied_at) + VALUES (?, datetime('now'));"; + + connection.Execute(insertSql, migrationName); + + // 트랜잭션 커밋 + connection.Commit(); + ToolkitLogger.LogDebug("MigrationRunner", $" 마이그레이션 트랜잭션 커밋 완료: {migrationName}"); + } + catch (Exception ex) + { + // 트랜잭션 롤백 + connection.Rollback(); + ToolkitLogger.LogError("MigrationRunner", $" 마이그레이션 실패 - 롤백됨: {ex.Message}"); + throw; + } + }, cancellationToken: cancellationToken); + + return new MigrationResult { Success = true }; + } + catch (Exception ex) + { + return new MigrationResult + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + #endregion + + #region Migration Table Management + /// + /// migrations 테이블 생성 (존재하지 않으면) + /// + private async UniTask EnsureMigrationTableExistsAsync(CancellationToken cancellationToken) + { + string createTableSql = @" + CREATE TABLE IF NOT EXISTS migrations ( + migration_id INTEGER PRIMARY KEY AUTOINCREMENT, + migration_name TEXT NOT NULL UNIQUE, + applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_migrations_name ON migrations(migration_name); + "; + + await UniTask.RunOnThreadPool(() => + { + var connection = databaseManager.Connector.Connection; + connection.Execute(createTableSql); + }, cancellationToken: cancellationToken); + + ToolkitLogger.LogDebug("MigrationRunner", "migrations 테이블 확인 완료."); + } + + /// + /// 실행된 마이그레이션 목록 조회 + /// + private async UniTask> GetAppliedMigrationsAsync(CancellationToken cancellationToken) + { + var appliedMigrations = new List(); + + string selectSql = "SELECT migration_name FROM migrations ORDER BY migration_id ASC;"; + + await UniTask.RunOnThreadPool(() => + { + var connection = databaseManager.Connector.Connection; + var results = connection.Query(selectSql); + + foreach (var record in results) + { + appliedMigrations.Add(record.migration_name); + } + }, cancellationToken: cancellationToken); + + return appliedMigrations; + } + + /// + /// Migration 레코드 (SQLite 쿼리 결과용) + /// + private class MigrationRecord + { + public string migration_name { get; set; } + } + #endregion + + #region Pending Migration Check + /// + /// 대기 중인 마이그레이션 수 조회 (UI 표시용) + /// + public async UniTask GetPendingMigrationCountAsync(CancellationToken cancellationToken = default) + { + if (!databaseManager.IsInitialized || !databaseManager.IsConnected) + { + return -1; // 연결 안됨 + } + + try + { + // migrations 테이블 확인 + await EnsureMigrationTableExistsAsync(cancellationToken); + + // 실행된 마이그레이션 목록 조회 + var appliedMigrations = await GetAppliedMigrationsAsync(cancellationToken); + + // 마이그레이션 파일 목록 조회 + var migrationFiles = GetMigrationFiles(); + + // 미실행 마이그레이션 필터링 + var pendingMigrations = migrationFiles + .Where(file => !appliedMigrations.Contains(Path.GetFileNameWithoutExtension(file))) + .ToList(); + + return pendingMigrations.Count; + } + catch (Exception ex) + { + ToolkitLogger.LogWarning("MigrationRunner", $" 펜딩 마이그레이션 확인 실패: {ex.Message}"); + return -1; + } + } + #endregion + + #region File Discovery + /// + /// 마이그레이션 파일 목록 조회 (.sql 파일) + /// + private List GetMigrationFiles() + { + if (!Directory.Exists(migrationsPath)) + { + ToolkitLogger.LogWarning("MigrationRunner", $" Migrations 폴더가 존재하지 않습니다: {migrationsPath}"); + return new List(); + } + + var files = Directory.GetFiles(migrationsPath, "*.sql", SearchOption.TopDirectoryOnly) + .OrderBy(file => file) + .ToList(); + + return files; + } + + /// + /// SQL 문에서 주석을 제거 (Execute() 호출 전 유효성 검증용) + /// + private string RemoveSqlComments(string sql) + { + var result = new System.Text.StringBuilder(); + bool inSingleLineComment = false; + bool inMultiLineComment = false; + bool inString = false; + + for (int i = 0; i < sql.Length; i++) + { + char c = sql[i]; + char nextChar = i + 1 < sql.Length ? sql[i + 1] : '\0'; + + // 줄바꿈 처리 (단일 줄 주석 종료) + if (c == '\n' || c == '\r') + { + inSingleLineComment = false; + if (!inMultiLineComment) + { + result.Append(c); + } + continue; + } + + // 단일 줄 주석 시작 + if (!inString && !inMultiLineComment && c == '-' && nextChar == '-') + { + inSingleLineComment = true; + i++; // 두 번째 '-' 스킵 + continue; + } + + // 다중 줄 주석 시작 + if (!inString && !inSingleLineComment && c == '/' && nextChar == '*') + { + inMultiLineComment = true; + i++; // '*' 스킵 + continue; + } + + // 다중 줄 주석 종료 + if (inMultiLineComment && c == '*' && nextChar == '/') + { + inMultiLineComment = false; + i++; // '/' 스킵 + continue; + } + + // 주석 내부면 스킵 + if (inSingleLineComment || inMultiLineComment) + { + continue; + } + + // 문자열 시작/종료 (작은따옴표) + if (c == '\'') + { + // 이스케이프된 따옴표 확인 ('') + if (inString && nextChar == '\'') + { + result.Append(c); + i++; // 다음 따옴표도 추가 + result.Append('\''); + continue; + } + inString = !inString; + } + + result.Append(c); + } + + return result.ToString(); + } + + /// + /// SQL 문장을 BEGIN...END 블록을 고려하여 분리 + /// + private List SplitSqlStatements(string sql) + { + var statements = new List(); + var currentStatement = new System.Text.StringBuilder(); + int beginEndDepth = 0; + bool inSingleLineComment = false; + bool inMultiLineComment = false; + bool inString = false; + + for (int i = 0; i < sql.Length; i++) + { + char c = sql[i]; + char nextChar = i + 1 < sql.Length ? sql[i + 1] : '\0'; + + // 줄바꿈 처리 (단일 줄 주석 종료) + if (c == '\n') + { + inSingleLineComment = false; + currentStatement.Append(c); + continue; + } + + // 단일 줄 주석 시작 (문자열 내부가 아닐 때) + if (!inString && !inMultiLineComment && c == '-' && nextChar == '-') + { + inSingleLineComment = true; + currentStatement.Append(c); + continue; + } + + // 다중 줄 주석 시작 + if (!inString && !inSingleLineComment && c == '/' && nextChar == '*') + { + inMultiLineComment = true; + currentStatement.Append(c); + continue; + } + + // 다중 줄 주석 종료 + if (inMultiLineComment && c == '*' && nextChar == '/') + { + inMultiLineComment = false; + currentStatement.Append(c); + i++; // '/' 스킵 + currentStatement.Append('/'); + continue; + } + + // 주석 내부면 그대로 추가 + if (inSingleLineComment || inMultiLineComment) + { + currentStatement.Append(c); + continue; + } + + // 문자열 시작/종료 (작은따옴표) + if (c == '\'') + { + // 이스케이프된 따옴표 확인 ('') + if (inString && nextChar == '\'') + { + currentStatement.Append(c); + i++; // 다음 따옴표도 추가 + currentStatement.Append('\''); + continue; + } + inString = !inString; + currentStatement.Append(c); + continue; + } + + // 문자열 내부면 그대로 추가 + if (inString) + { + currentStatement.Append(c); + continue; + } + + // BEGIN 키워드 감지 (대소문자 무시) + if (i + 5 <= sql.Length) + { + string word = sql.Substring(i, 5).ToUpper(); + if (word == "BEGIN" && (i == 0 || !char.IsLetterOrDigit(sql[i - 1])) && + (i + 5 >= sql.Length || !char.IsLetterOrDigit(sql[i + 5]))) + { + beginEndDepth++; + } + } + + // END 키워드 감지 + if (i + 3 <= sql.Length) + { + string word = sql.Substring(i, 3).ToUpper(); + if (word == "END" && (i == 0 || !char.IsLetterOrDigit(sql[i - 1])) && + (i + 3 >= sql.Length || !char.IsLetterOrDigit(sql[i + 3]))) + { + beginEndDepth--; + } + } + + // 세미콜론으로 문장 분리 (BEGIN...END 블록 외부에서만) + if (c == ';' && beginEndDepth == 0) + { + currentStatement.Append(c); + string statement = currentStatement.ToString().Trim(); + if (!string.IsNullOrWhiteSpace(statement)) + { + statements.Add(statement); + } + currentStatement.Clear(); + continue; + } + + currentStatement.Append(c); + } + + // 마지막 문장 추가 (세미콜론 없이 끝나는 경우) + string lastStatement = currentStatement.ToString().Trim(); + if (!string.IsNullOrWhiteSpace(lastStatement)) + { + statements.Add(lastStatement); + } + + return statements; + } + #endregion + } + + #region Result Structs + /// + /// 마이그레이션 결과 + /// + public struct MigrationResult + { + public bool Success; + public string Message; + public string ErrorMessage; + public int MigrationsApplied; + } + #endregion +} diff --git a/skills/assets/unity-package/Editor/Database/MigrationRunner.cs.meta b/skills/assets/unity-package/Editor/Database/MigrationRunner.cs.meta new file mode 100644 index 0000000..40fe434 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/MigrationRunner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7efe4a498e5f5254ba0be804329e8fd1 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/Migrations.meta b/skills/assets/unity-package/Editor/Database/Migrations.meta new file mode 100644 index 0000000..54667af --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Migrations.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1056308bc40a585459dd7361bc65fd71 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql b/skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql new file mode 100644 index 0000000..94ecc36 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql @@ -0,0 +1,358 @@ +-- Migration 001: Initial Schema (SQLite) +-- Unity Editor Toolkit - SQLite Database Schema +-- Embedded SQLite (설치 불필요) +-- Created: 2025-11-14 +-- Updated: 2025-11-14 (PostgreSQL → SQLite) + +-- ============================================================ +-- PRAGMA 설정 +-- ============================================================ + +-- 참고: PRAGMA 설정은 SQLiteConnector.ConnectAsync()에서 자동으로 적용됩니다. +-- - foreign_keys = ON (Foreign Key 제약 활성화) +-- - journal_mode = WAL (Write-Ahead Logging, 성능 향상) +-- - synchronous = NORMAL (fsync 최적화) + +-- ============================================================ +-- TABLE 1: scenes +-- 씬 정보 (프로젝트의 모든 씬) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS scenes ( + scene_id INTEGER PRIMARY KEY AUTOINCREMENT, + scene_name TEXT NOT NULL, + scene_path TEXT NOT NULL UNIQUE, + build_index INTEGER, + is_loaded INTEGER NOT NULL DEFAULT 0, -- BOOLEAN → INTEGER (0/1) + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_scenes_name ON scenes(scene_name); +CREATE INDEX idx_scenes_is_loaded ON scenes(is_loaded); + +-- ============================================================ +-- TABLE 2: gameobjects +-- GameObject 정보 (Closure Table로 계층 구조 관리) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS gameobjects ( + object_id INTEGER PRIMARY KEY AUTOINCREMENT, + instance_id INTEGER NOT NULL UNIQUE, + scene_id INTEGER REFERENCES scenes(scene_id) ON DELETE CASCADE, + object_name TEXT NOT NULL, + parent_id INTEGER REFERENCES gameobjects(object_id) ON DELETE SET NULL, + tag TEXT DEFAULT 'Untagged', + layer INTEGER DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 1, -- BOOLEAN → INTEGER + is_static INTEGER NOT NULL DEFAULT 0, -- BOOLEAN → INTEGER + is_deleted INTEGER NOT NULL DEFAULT 0, -- BOOLEAN → INTEGER (Soft delete) + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 인덱스 +CREATE INDEX idx_gameobjects_instance_id ON gameobjects(instance_id); +CREATE INDEX idx_gameobjects_scene_id ON gameobjects(scene_id); +CREATE INDEX idx_gameobjects_parent_id ON gameobjects(parent_id); +CREATE INDEX idx_gameobjects_tag ON gameobjects(tag); +CREATE INDEX idx_gameobjects_is_active ON gameobjects(is_active); +CREATE INDEX idx_gameobjects_is_deleted ON gameobjects(is_deleted); + +-- ============================================================ +-- TABLE 2-1: gameobject_closure +-- GameObject 계층 구조 (Closure Table) +-- PostgreSQL ltree 대체 - 성능이 더 우수함 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS gameobject_closure ( + ancestor_id INTEGER NOT NULL, + descendant_id INTEGER NOT NULL, + depth INTEGER NOT NULL, + PRIMARY KEY (ancestor_id, descendant_id), + FOREIGN KEY (ancestor_id) REFERENCES gameobjects(object_id) ON DELETE CASCADE, + FOREIGN KEY (descendant_id) REFERENCES gameobjects(object_id) ON DELETE CASCADE +); + +-- 인덱스 +CREATE INDEX idx_closure_ancestor ON gameobject_closure(ancestor_id); +CREATE INDEX idx_closure_descendant ON gameobject_closure(descendant_id); +CREATE INDEX idx_closure_depth ON gameobject_closure(depth); + +-- ============================================================ +-- TABLE 3: components +-- Component 정보 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS components ( + component_id INTEGER PRIMARY KEY AUTOINCREMENT, + object_id INTEGER NOT NULL REFERENCES gameobjects(object_id) ON DELETE CASCADE, + component_type TEXT NOT NULL, + component_data TEXT CHECK(json_valid(component_data)), -- JSON validation + is_enabled INTEGER NOT NULL DEFAULT 1, -- BOOLEAN → INTEGER + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_components_object_id ON components(object_id); +CREATE INDEX idx_components_type ON components(component_type); +-- SQLite JSON index는 json_each/json_tree로 쿼리 시 자동 활용 + +-- ============================================================ +-- TABLE 4: transforms +-- Transform 히스토리 (위치, 회전, 스케일 변경 추적) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS transforms ( + transform_id INTEGER PRIMARY KEY AUTOINCREMENT, + object_id INTEGER NOT NULL REFERENCES gameobjects(object_id) ON DELETE CASCADE, + position_x REAL NOT NULL, + position_y REAL NOT NULL, + position_z REAL NOT NULL, + rotation_x REAL NOT NULL, -- Quaternion X + rotation_y REAL NOT NULL, -- Quaternion Y + rotation_z REAL NOT NULL, -- Quaternion Z + rotation_w REAL NOT NULL, -- Quaternion W + scale_x REAL NOT NULL, + scale_y REAL NOT NULL, + scale_z REAL NOT NULL, + recorded_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_transforms_object_id ON transforms(object_id); +CREATE INDEX idx_transforms_recorded_at ON transforms(recorded_at); + +-- ============================================================ +-- TABLE 5: command_history +-- 명령 히스토리 (Command Pattern, Undo/Redo 지원) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS command_history ( + -- Primary Key: GUID 문자열 (C# Guid.NewGuid().ToString()) + command_id TEXT PRIMARY KEY, + + -- Command 정보 + command_name TEXT NOT NULL, + command_type TEXT NOT NULL, + command_data TEXT NOT NULL CHECK(json_valid(command_data)), -- JSON validation + + -- 실행 정보 + executed_at TEXT NOT NULL, + executed_by TEXT DEFAULT 'System', + + -- 메타데이터 + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 인덱스 +CREATE INDEX idx_command_history_executed_at ON command_history(executed_at DESC); +CREATE INDEX idx_command_history_type ON command_history(command_type); +CREATE INDEX idx_command_history_name ON command_history(command_name); +CREATE INDEX idx_command_history_executed_by ON command_history(executed_by); + +-- ============================================================ +-- TABLE 6: snapshots +-- 씬 스냅샷 (시점 복원용) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS snapshots ( + snapshot_id INTEGER PRIMARY KEY AUTOINCREMENT, + scene_id INTEGER NOT NULL REFERENCES scenes(scene_id) ON DELETE CASCADE, + snapshot_name TEXT NOT NULL, + snapshot_data TEXT NOT NULL CHECK(json_valid(snapshot_data)), -- JSON validation + description TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_snapshots_scene_id ON snapshots(scene_id); +CREATE INDEX idx_snapshots_created_at ON snapshots(created_at); + +-- ============================================================ +-- TABLE 7: metadata +-- 프로젝트 메타데이터 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS metadata ( + metadata_id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + value TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_metadata_key ON metadata(key); + +-- ============================================================ +-- TABLE 8: analytics_cache +-- 분석 결과 캐시 (성능 최적화) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS analytics_cache ( + cache_id INTEGER PRIMARY KEY AUTOINCREMENT, + cache_key TEXT NOT NULL UNIQUE, + cache_data TEXT NOT NULL CHECK(json_valid(cache_data)), -- JSON validation + expires_at TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_analytics_cache_key ON analytics_cache(cache_key); +CREATE INDEX idx_analytics_cache_expires_at ON analytics_cache(expires_at); + +-- ============================================================ +-- TRIGGERS +-- ============================================================ + +-- gameobjects.updated_at 자동 업데이트 +CREATE TRIGGER IF NOT EXISTS trigger_gameobjects_updated_at +AFTER UPDATE ON gameobjects +FOR EACH ROW +BEGIN + UPDATE gameobjects SET updated_at = CURRENT_TIMESTAMP + WHERE object_id = NEW.object_id; +END; + +-- components.updated_at 자동 업데이트 +CREATE TRIGGER IF NOT EXISTS trigger_components_updated_at +AFTER UPDATE ON components +FOR EACH ROW +BEGIN + UPDATE components SET updated_at = CURRENT_TIMESTAMP + WHERE component_id = NEW.component_id; +END; + +-- scenes.updated_at 자동 업데이트 +CREATE TRIGGER IF NOT EXISTS trigger_scenes_updated_at +AFTER UPDATE ON scenes +FOR EACH ROW +BEGIN + UPDATE scenes SET updated_at = CURRENT_TIMESTAMP + WHERE scene_id = NEW.scene_id; +END; + +-- metadata.updated_at 자동 업데이트 +CREATE TRIGGER IF NOT EXISTS trigger_metadata_updated_at +AFTER UPDATE ON metadata +FOR EACH ROW +BEGIN + UPDATE metadata SET updated_at = CURRENT_TIMESTAMP + WHERE metadata_id = NEW.metadata_id; +END; + +-- ============================================================ +-- CLOSURE TABLE TRIGGERS (자동 유지 관리) +-- ============================================================ + +-- GameObject 생성 시: 자기 자신 + 부모 조상들 추가 +CREATE TRIGGER IF NOT EXISTS trigger_gameobject_insert_closure +AFTER INSERT ON gameobjects +FOR EACH ROW +BEGIN + -- 1. 자기 자신 (depth = 0) + INSERT INTO gameobject_closure (ancestor_id, descendant_id, depth) + VALUES (NEW.object_id, NEW.object_id, 0); + + -- 2. 부모가 있으면 부모의 모든 조상 복사 (depth + 1) + INSERT INTO gameobject_closure (ancestor_id, descendant_id, depth) + SELECT ancestor_id, NEW.object_id, depth + 1 + FROM gameobject_closure + WHERE descendant_id = NEW.parent_id AND NEW.parent_id IS NOT NULL; +END; + +-- GameObject 삭제 시: 관련 Closure 레코드 자동 삭제 (CASCADE) +-- (FOREIGN KEY ON DELETE CASCADE가 처리) + +-- ============================================================ +-- VIEWS +-- ============================================================ + +-- Active GameObjects View (is_deleted = 0, is_active = 1) +CREATE VIEW IF NOT EXISTS active_gameobjects AS +SELECT + object_id, + instance_id, + scene_id, + object_name, + parent_id, + tag, + layer, + created_at, + updated_at +FROM gameobjects +WHERE is_deleted = 0 AND is_active = 1; + +-- GameObject Component Count View +CREATE VIEW IF NOT EXISTS gameobject_component_count AS +SELECT + g.object_id, + g.object_name, + g.parent_id, + COUNT(c.component_id) AS component_count +FROM gameobjects g +LEFT JOIN components c ON g.object_id = c.object_id +WHERE g.is_deleted = 0 +GROUP BY g.object_id, g.object_name, g.parent_id; + +-- Recent Commands View +CREATE VIEW IF NOT EXISTS recent_commands AS +SELECT + command_id, + command_name, + command_type, + executed_at, + executed_by +FROM command_history +ORDER BY executed_at DESC +LIMIT 100; + +-- Command Statistics View +CREATE VIEW IF NOT EXISTS command_statistics AS +SELECT + command_type, + COUNT(*) AS command_count, + MIN(executed_at) AS first_executed, + MAX(executed_at) AS last_executed +FROM command_history +GROUP BY command_type +ORDER BY command_count DESC; + +-- ============================================================ +-- INITIAL DATA +-- ============================================================ + +-- 기본 메타데이터 삽입 +INSERT OR IGNORE INTO metadata (key, value) VALUES + ('schema_version', '1'), + ('database_type', 'SQLite'), + ('database_created_at', datetime('now')), + ('unity_editor_toolkit_version', '0.5.0'); + +-- ============================================================ +-- COMPLETION +-- ============================================================ + +-- SQLite는 RAISE NOTICE가 없으므로 SELECT로 완료 메시지 출력 +SELECT '==========================================================' AS message; +SELECT 'Migration 001: Initial Schema (SQLite) - 완료' AS message; +SELECT '==========================================================' AS message; +SELECT '생성된 테이블: 9개' AS message; +SELECT ' - scenes, gameobjects, gameobject_closure, components' AS message; +SELECT ' - transforms, command_history, snapshots, metadata, analytics_cache' AS message; +SELECT '' AS message; +SELECT '생성된 인덱스: 23개' AS message; +SELECT ' - gameobject_closure: 3개 (ancestor, descendant, depth)' AS message; +SELECT ' - command_history: 4개 (GUID, executed_at, type, name, executed_by)' AS message; +SELECT '' AS message; +SELECT '생성된 트리거: 5개' AS message; +SELECT ' - updated_at 자동 업데이트 (gameobjects, components, scenes, metadata)' AS message; +SELECT ' - Closure Table 자동 유지 (gameobject_insert_closure)' AS message; +SELECT '' AS message; +SELECT '생성된 뷰: 4개' AS message; +SELECT ' - active_gameobjects, gameobject_component_count' AS message; +SELECT ' - recent_commands, command_statistics' AS message; +SELECT '' AS message; +SELECT 'Closure Table 계층 구조 쿼리 예시:' AS message; +SELECT ' -- 특정 GameObject의 모든 자식 (ltree 대체):' AS message; +SELECT ' SELECT g.* FROM gameobjects g' AS message; +SELECT ' INNER JOIN gameobject_closure c ON g.object_id = c.descendant_id' AS message; +SELECT ' WHERE c.ancestor_id = ? AND c.depth > 0;' AS message; +SELECT '==========================================================' AS message; diff --git a/skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql.meta b/skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql.meta new file mode 100644 index 0000000..5692a92 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Migrations/Migration_001_InitialSchema.sql.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5345bdb2f717d2e409cdd34c2a7262a3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql b/skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql new file mode 100644 index 0000000..098ef8e --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql @@ -0,0 +1,31 @@ +-- Migration 002: Add GUID to GameObject +-- Unity Editor Toolkit - Add persistent GUID column to gameobjects table +-- Created: 2025-11-18 +-- Purpose: Replace instance_id with GUID for persistent GameObject identification + +-- ============================================================ +-- ALTER TABLE: gameobjects +-- Add guid column for persistent identification +-- ============================================================ + +-- Add guid column (nullable initially for migration) +ALTER TABLE gameobjects ADD COLUMN guid TEXT; + +-- Create unique index on guid (after data migration) +-- Note: Index will be created after populating existing rows with GUIDs +-- CREATE UNIQUE INDEX idx_gameobjects_guid ON gameobjects(guid); + +-- ============================================================ +-- Migration Notes +-- ============================================================ +-- +-- GUID migration strategy: +-- 1. Add guid column (nullable) +-- 2. Existing rows will have NULL guid +-- 3. SyncManager will generate GUIDs when syncing GameObjects +-- 4. GameObjectGuid component ensures all GameObjects have GUIDs +-- 5. After full scene sync, make guid NOT NULL and UNIQUE +-- +-- Future migration (Migration_003): +-- ALTER TABLE gameobjects ALTER COLUMN guid SET NOT NULL; +-- CREATE UNIQUE INDEX idx_gameobjects_guid ON gameobjects(guid); diff --git a/skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql.meta b/skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql.meta new file mode 100644 index 0000000..0f26f95 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Migrations/Migration_002_AddGameObjectGuid.sql.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: d06228fa3660f4f43a33b92f2cc4e994 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: ba03993677dd740da91b9a13653538e9, type: 3} diff --git a/skills/assets/unity-package/Editor/Database/Models.meta b/skills/assets/unity-package/Editor/Database/Models.meta new file mode 100644 index 0000000..ec3ef75 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Models.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b6b69cd3d00bc704c863538202e5efb3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/skills/assets/unity-package/Editor/Database/Models/.gitkeep b/skills/assets/unity-package/Editor/Database/Models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/assets/unity-package/Editor/Database/Queries.meta b/skills/assets/unity-package/Editor/Database/Queries.meta new file mode 100644 index 0000000..d908bf2 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Queries.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8952d02cfc5f21e4b82dc98c5e3e0d21 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/skills/assets/unity-package/Editor/Database/Queries/.gitkeep b/skills/assets/unity-package/Editor/Database/Queries/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/assets/unity-package/Editor/Database/SQLiteConnector.cs b/skills/assets/unity-package/Editor/Database/SQLiteConnector.cs new file mode 100644 index 0000000..dc732ef --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/SQLiteConnector.cs @@ -0,0 +1,344 @@ +using System; +using System.Threading; +using Cysharp.Threading.Tasks; +using SQLite; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database +{ + /// + /// SQLite 데이터베이스 커넥터 + /// 임베디드 SQLite - 설치 불필요 + /// + public class SQLiteConnector + { + #region Fields + private readonly DatabaseConfig config; + private SQLiteConnection connection; + private bool isConnected; + #endregion + + #region Properties + /// + /// 연결 상태 + /// + public bool IsConnected => isConnected && connection != null; + + /// + /// SQLite 연결 객체 + /// + public SQLiteConnection Connection => connection; + #endregion + + #region Constructor + public SQLiteConnector(DatabaseConfig config) + { + this.config = config ?? throw new ArgumentNullException(nameof(config)); + } + #endregion + + #region Connection Management + /// + /// 데이터베이스 연결 + /// + public async UniTask ConnectAsync(CancellationToken cancellationToken = default) + { + try + { + if (isConnected) + { + ToolkitLogger.LogWarning("SQLiteConnector", "Already connected."); + return new ConnectionResult + { + Success = true, + Message = "Already connected." + }; + } + + ToolkitLogger.Log("SQLiteConnector", $" Connecting to: {config.DatabaseFilePath}"); + + // SQLite 연결 생성 (동기 작업이지만 UniTask로 래핑) + await UniTask.RunOnThreadPool(() => + { + // SQLite 연결 옵션 설정 + var options = new SQLiteConnectionString( + config.DatabaseFilePath, + SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.FullMutex, + storeDateTimeAsTicks: true + ); + + connection = new SQLiteConnection(options); + + // WAL 모드 설정 (성능 향상) + if (config.EnableWAL) + { + try + { + connection.Execute("PRAGMA journal_mode=WAL;"); + ToolkitLogger.Log("SQLiteConnector", "WAL mode enabled."); + } + catch (Exception walEx) + { + ToolkitLogger.LogWarning("SQLiteConnector", $" WAL mode failed (continuing without WAL): {walEx.Message}"); + } + } + + // Foreign Keys 활성화 + try + { + connection.Execute("PRAGMA foreign_keys=ON;"); + } + catch (Exception fkEx) + { + ToolkitLogger.LogWarning("SQLiteConnector", $" Foreign keys activation failed: {fkEx.Message}"); + } + + // Synchronous 설정 (NORMAL = 안전하면서도 빠름) + try + { + connection.Execute("PRAGMA synchronous=NORMAL;"); + } + catch (Exception syncEx) + { + ToolkitLogger.LogWarning("SQLiteConnector", $" Synchronous setting failed: {syncEx.Message}"); + } + + }, cancellationToken: cancellationToken); + + isConnected = true; + + ToolkitLogger.Log("SQLiteConnector", $" Connected successfully: {config.DatabaseFilePath}"); + return new ConnectionResult + { + Success = true, + Message = "Connection successful." + }; + } + catch (OperationCanceledException) + { + ToolkitLogger.LogWarning("SQLiteConnector", "Connection cancelled."); + return new ConnectionResult + { + Success = false, + ErrorMessage = "Connection cancelled." + }; + } + catch (Exception ex) + { + ToolkitLogger.LogError("SQLiteConnector", $" Connection failed: {ex.Message}\n{ex.StackTrace}"); + return new ConnectionResult + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + + /// + /// 데이터베이스 연결 해제 (비동기) + /// + public async UniTask DisconnectAsync() + { + try + { + if (!isConnected) + { + return; + } + + ToolkitLogger.Log("SQLiteConnector", "Disconnecting..."); + + await UniTask.RunOnThreadPool(() => + { + DisconnectInternal(); + }); + + isConnected = false; + + // 강제 GC (파일 핸들 해제를 위해) + GC.Collect(); + GC.WaitForPendingFinalizers(); + + ToolkitLogger.Log("SQLiteConnector", "Disconnected."); + } + catch (Exception ex) + { + ToolkitLogger.LogError("SQLiteConnector", $" Disconnect error: {ex.Message}"); + } + } + + /// + /// 데이터베이스 연결 해제 (동기 - Assembly Reload/서버 종료 시 사용) + /// + public void Disconnect() + { + try + { + if (!isConnected) + { + return; + } + + ToolkitLogger.Log("SQLiteConnector", "Disconnecting (sync)..."); + + DisconnectInternal(); + + isConnected = false; + + ToolkitLogger.Log("SQLiteConnector", "Disconnected (sync)."); + } + catch (Exception ex) + { + ToolkitLogger.LogError("SQLiteConnector", $" Disconnect error: {ex.Message}"); + } + } + + /// + /// 내부 연결 해제 로직 + /// + private void DisconnectInternal() + { + // WAL 체크포인트 수행 (파일 잠금 해제를 위해) + if (config.EnableWAL && connection != null) + { + try + { + connection.Execute("PRAGMA wal_checkpoint(TRUNCATE);"); + ToolkitLogger.Log("SQLiteConnector", "WAL checkpoint completed."); + } + catch (Exception walEx) + { + ToolkitLogger.LogWarning("SQLiteConnector", $" WAL checkpoint failed: {walEx.Message}"); + } + } + + connection?.Close(); + connection?.Dispose(); + connection = null; + } + + /// + /// 연결 테스트 + /// + public async UniTask TestConnectionAsync(CancellationToken cancellationToken = default) + { + try + { + if (!isConnected || connection == null) + { + return false; + } + + // 간단한 쿼리로 연결 테스트 + await UniTask.RunOnThreadPool(() => + { + connection.ExecuteScalar("SELECT 1;"); + }, cancellationToken: cancellationToken); + + return true; + } + catch (Exception ex) + { + ToolkitLogger.LogError("SQLiteConnector", $" Connection test failed: {ex.Message}"); + return false; + } + } + #endregion + + #region Database Operations + /// + /// SQL 스크립트 실행 (마이그레이션용) + /// + public async UniTask ExecuteScriptAsync(string sql, CancellationToken cancellationToken = default) + { + if (!isConnected || connection == null) + { + throw new InvalidOperationException("Database is not connected."); + } + + try + { + return await UniTask.RunOnThreadPool(() => + { + return connection.Execute(sql); + }, cancellationToken: cancellationToken); + } + catch (Exception ex) + { + ToolkitLogger.LogError("SQLiteConnector", $" Execute script failed: {ex.Message}\n{sql}"); + throw; + } + } + + /// + /// 단일 값 조회 + /// + public async UniTask ExecuteScalarAsync(string sql, CancellationToken cancellationToken = default) + { + if (!isConnected || connection == null) + { + throw new InvalidOperationException("Database is not connected."); + } + + try + { + return await UniTask.RunOnThreadPool(() => + { + return connection.ExecuteScalar(sql); + }, cancellationToken: cancellationToken); + } + catch (Exception ex) + { + ToolkitLogger.LogError("SQLiteConnector", $" Execute scalar failed: {ex.Message}\n{sql}"); + throw; + } + } + + /// + /// 데이터베이스 파일 존재 여부 확인 + /// + public bool DatabaseFileExists() + { + return System.IO.File.Exists(config.DatabaseFilePath); + } + + /// + /// SQLite 버전 조회 + /// + public async UniTask GetDatabaseVersionAsync() + { + if (!isConnected || connection == null) + { + return "Not Connected"; + } + + try + { + return await UniTask.RunOnThreadPool(() => + { + var result = connection.ExecuteScalar("SELECT sqlite_version();"); + return $"SQLite {result}"; + }); + } + catch (Exception ex) + { + ToolkitLogger.LogError("SQLiteConnector", $" Failed to get version: {ex.Message}"); + return "Unknown"; + } + } + #endregion + } + + #region Result Struct + /// + /// 연결 결과 + /// + public struct ConnectionResult + { + public bool Success; + public string Message; + public string ErrorMessage; + } + #endregion +} diff --git a/skills/assets/unity-package/Editor/Database/SQLiteConnector.cs.meta b/skills/assets/unity-package/Editor/Database/SQLiteConnector.cs.meta new file mode 100644 index 0000000..439b124 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/SQLiteConnector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ad42d3641d3371b4d890f2ebed149635 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/Setup.meta b/skills/assets/unity-package/Editor/Database/Setup.meta new file mode 100644 index 0000000..1664dfc --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Setup.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b8dc1710d9b974e47a258b98c60eb5a6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs b/skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs new file mode 100644 index 0000000..4d01332 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs @@ -0,0 +1,182 @@ +using System; +using System.IO; +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database.Setup +{ + /// + /// SQLite 데이터베이스 자동 생성 + /// 파일 기반 DB이므로 설치 불필요 + /// + public class DatabaseCreator + { + /// + /// 데이터베이스 파일 생성 및 확인 + /// + public async UniTask CreateDatabaseAsync(DatabaseConfig config, CancellationToken cancellationToken = default) + { + try + { + ToolkitLogger.Log("DatabaseCreator", $" 데이터베이스 확인 시작: {config.DatabaseFilePath}"); + + // 1. 데이터베이스 파일 존재 여부 확인 + if (File.Exists(config.DatabaseFilePath)) + { + long fileSize = new FileInfo(config.DatabaseFilePath).Length; + ToolkitLogger.Log("DatabaseCreator", $" 데이터베이스 파일이 이미 존재합니다: {config.DatabaseFilePath} ({fileSize} bytes)"); + + return new DatabaseCreationResult + { + Success = true, + Message = $"Database file already exists: {config.DatabaseFilePath}", + AlreadyExists = true + }; + } + + // 2. 디렉토리 생성 (존재하지 않으면) + string directory = Path.GetDirectoryName(config.DatabaseFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + ToolkitLogger.Log("DatabaseCreator", $" 디렉토리 생성: {directory}"); + Directory.CreateDirectory(directory); + } + + // 3. SQLite 파일은 첫 연결 시 자동 생성됨 + // 여기서는 빈 파일을 생성하지 않고, 연결 시 자동 생성되도록 함 + ToolkitLogger.Log("DatabaseCreator", $" 데이터베이스 준비 완료: {config.DatabaseFilePath}"); + ToolkitLogger.Log("DatabaseCreator", "첫 연결 시 자동으로 생성됩니다."); + + await UniTask.Yield(cancellationToken); + + return new DatabaseCreationResult + { + Success = true, + Message = $"Database ready to be created: {config.DatabaseFilePath}", + AlreadyExists = false + }; + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseCreator", $" 예외 발생: {ex.Message}\n{ex.StackTrace}"); + return new DatabaseCreationResult + { + Success = false, + ErrorMessage = ex.Message, + AlreadyExists = false + }; + } + } + + /// + /// 데이터베이스 파일 삭제 (개발/테스트용) + /// + public async UniTask DeleteDatabaseAsync(DatabaseConfig config, CancellationToken cancellationToken = default) + { + try + { + if (!File.Exists(config.DatabaseFilePath)) + { + ToolkitLogger.LogWarning("DatabaseCreator", $" 데이터베이스 파일이 존재하지 않습니다: {config.DatabaseFilePath}"); + return true; + } + + ToolkitLogger.Log("DatabaseCreator", $" 데이터베이스 삭제: {config.DatabaseFilePath}"); + File.Delete(config.DatabaseFilePath); + + // WAL 파일도 삭제 (있으면) + string walFile = config.DatabaseFilePath + "-wal"; + string shmFile = config.DatabaseFilePath + "-shm"; + + if (File.Exists(walFile)) + { + File.Delete(walFile); + ToolkitLogger.Log("DatabaseCreator", $" WAL 파일 삭제: {walFile}"); + } + + if (File.Exists(shmFile)) + { + File.Delete(shmFile); + ToolkitLogger.Log("DatabaseCreator", $" SHM 파일 삭제: {shmFile}"); + } + + await UniTask.Yield(cancellationToken); + + ToolkitLogger.Log("DatabaseCreator", "데이터베이스 삭제 완료."); + return true; + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseCreator", $" 삭제 실패: {ex.Message}"); + return false; + } + } + + /// + /// 데이터베이스 파일 정보 조회 + /// + public DatabaseFileInfo GetDatabaseInfo(DatabaseConfig config) + { + if (!File.Exists(config.DatabaseFilePath)) + { + return new DatabaseFileInfo + { + Exists = false, + FilePath = config.DatabaseFilePath + }; + } + + var fileInfo = new FileInfo(config.DatabaseFilePath); + + return new DatabaseFileInfo + { + Exists = true, + FilePath = config.DatabaseFilePath, + FileSize = fileInfo.Length, + CreatedTime = fileInfo.CreationTime, + ModifiedTime = fileInfo.LastWriteTime + }; + } + } + + #region Result Structs + /// + /// 데이터베이스 생성 결과 + /// + public struct DatabaseCreationResult + { + public bool Success; + public string Message; + public string ErrorMessage; + public bool AlreadyExists; + } + + /// + /// 데이터베이스 파일 정보 + /// + public struct DatabaseFileInfo + { + public bool Exists; + public string FilePath; + public long FileSize; + public DateTime CreatedTime; + public DateTime ModifiedTime; + + public override string ToString() + { + if (!Exists) + { + return $"[DatabaseFileInfo] File does not exist: {FilePath}"; + } + + return $"[DatabaseFileInfo]\n" + + $" Path: {FilePath}\n" + + $" Size: {FileSize / 1024.0:F2} KB\n" + + $" Created: {CreatedTime}\n" + + $" Modified: {ModifiedTime}"; + } + } + #endregion +} diff --git a/skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs.meta b/skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs.meta new file mode 100644 index 0000000..b48a993 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Setup/DatabaseCreator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dea57a163338a004098266d197d522dd \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs b/skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs new file mode 100644 index 0000000..0d34eac --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs @@ -0,0 +1,222 @@ +using System; +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database.Setup +{ + /// + /// Database 원클릭 자동 설치 마법사 + /// SQLite - 설치 불필요, DB 파일 생성 → 마이그레이션 + /// + public class DatabaseSetupWizard + { + #region Setup Steps + public enum SetupStep + { + NotStarted, + PreparingDatabase, + RunningMigrations, + Completed, + Failed + } + #endregion + + #region State + private SetupStep currentStep = SetupStep.NotStarted; + private string statusMessage = ""; + private string errorMessage = ""; + private bool isRunning = false; + private float progress = 0f; + + public SetupStep CurrentStep => currentStep; + public string StatusMessage => statusMessage; + public string ErrorMessage => errorMessage; + public bool IsRunning => isRunning; + public float Progress => progress; + #endregion + + #region Dependencies + private readonly DatabaseCreator databaseCreator; + #endregion + + #region Constructor + public DatabaseSetupWizard() + { + databaseCreator = new DatabaseCreator(); + } + #endregion + + #region Main Setup Flow + /// + /// 자동 설치 시작 + /// + public async UniTask RunSetupAsync(DatabaseConfig config, CancellationToken cancellationToken = default) + { + if (isRunning) + { + return new SetupResult + { + Success = false, + ErrorMessage = "Setup already in progress." + }; + } + + isRunning = true; + currentStep = SetupStep.NotStarted; + errorMessage = ""; + progress = 0f; + + try + { + // Step 1: 데이터베이스 파일 준비 + if (!await PrepareDatabaseAsync(config, cancellationToken)) + { + return CreateFailureResult("데이터베이스 준비 실패."); + } + + // Step 2: 마이그레이션 실행 + if (!await RunMigrationsAsync(config, cancellationToken)) + { + return CreateFailureResult("마이그레이션 실행 실패."); + } + + // 완료 + currentStep = SetupStep.Completed; + statusMessage = "데이터베이스 설치 완료!"; + progress = 1f; + + ToolkitLogger.Log("DatabaseSetupWizard", "설치 완료!"); + + return new SetupResult + { + Success = true, + Message = "Database setup completed successfully!" + }; + } + catch (OperationCanceledException) + { + ToolkitLogger.LogWarning("DatabaseSetupWizard", "설치가 취소되었습니다."); + currentStep = SetupStep.Failed; + return CreateFailureResult("설치가 취소되었습니다."); + } + catch (Exception ex) + { + ToolkitLogger.LogError("DatabaseSetupWizard", $" 설치 중 예외 발생: {ex.Message}"); + currentStep = SetupStep.Failed; + return CreateFailureResult($"예외 발생: {ex.Message}"); + } + finally + { + isRunning = false; + } + } + #endregion + + #region Step Implementations + private async UniTask PrepareDatabaseAsync(DatabaseConfig config, CancellationToken cancellationToken) + { + currentStep = SetupStep.PreparingDatabase; + statusMessage = "데이터베이스 파일 준비 중..."; + progress = 0.3f; + + ToolkitLogger.Log("DatabaseSetupWizard", "데이터베이스 파일 준비 중..."); + + var result = await databaseCreator.CreateDatabaseAsync(config, cancellationToken); + + if (result.Success) + { + if (result.AlreadyExists) + { + ToolkitLogger.Log("DatabaseSetupWizard", $" 데이터베이스 파일이 이미 존재합니다: {config.DatabaseFilePath}"); + } + else + { + ToolkitLogger.Log("DatabaseSetupWizard", $" 데이터베이스 파일 준비 완료: {config.DatabaseFilePath}"); + } + return true; + } + else + { + errorMessage = $"데이터베이스 준비 실패: {result.ErrorMessage}"; + ToolkitLogger.LogError("DatabaseSetupWizard", $" {errorMessage}"); + return false; + } + } + + private async UniTask RunMigrationsAsync(DatabaseConfig config, CancellationToken cancellationToken) + { + currentStep = SetupStep.RunningMigrations; + statusMessage = "마이그레이션 실행 중..."; + progress = 0.6f; + + ToolkitLogger.Log("DatabaseSetupWizard", "마이그레이션 시작..."); + + try + { + // DatabaseManager 초기화 (InitializeAsync가 자동으로 마이그레이션도 실행함) + var initResult = await DatabaseManager.Instance.InitializeAsync(config); + if (!initResult.Success) + { + errorMessage = $"DatabaseManager 초기화 실패: {initResult.ErrorMessage}"; + ToolkitLogger.LogError("DatabaseSetupWizard", $" {errorMessage}"); + return false; + } + + // 마이그레이션은 InitializeAsync에서 자동으로 실행됨 + // 중복 실행을 방지하기 위해 별도 호출하지 않음 + ToolkitLogger.Log("DatabaseSetupWizard", "마이그레이션 완료 (DatabaseManager.InitializeAsync에서 실행됨)"); + return true; + } + catch (Exception ex) + { + errorMessage = $"마이그레이션 중 예외 발생: {ex.Message}"; + ToolkitLogger.LogError("DatabaseSetupWizard", $" {errorMessage}"); + return false; + } + } + #endregion + + #region Helper Methods + private SetupResult CreateFailureResult(string error) + { + currentStep = SetupStep.Failed; + errorMessage = error; + progress = 0f; + + return new SetupResult + { + Success = false, + ErrorMessage = error + }; + } + #endregion + + #region Reset + /// + /// 상태 초기화 + /// + public void Reset() + { + currentStep = SetupStep.NotStarted; + statusMessage = ""; + errorMessage = ""; + isRunning = false; + progress = 0f; + } + #endregion + } + + #region Result Struct + /// + /// Setup 결과 + /// + public struct SetupResult + { + public bool Success; + public string Message; + public string ErrorMessage; + } + #endregion +} diff --git a/skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs.meta b/skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs.meta new file mode 100644 index 0000000..a9c9a4f --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/Setup/DatabaseSetupWizard.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 64255a616a004d548b7250bb0302633f \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/Database/SyncManager.cs b/skills/assets/unity-package/Editor/Database/SyncManager.cs new file mode 100644 index 0000000..f2aa532 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/SyncManager.cs @@ -0,0 +1,866 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEditorToolkit.Runtime; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor.Database +{ + /// + /// Unity ↔ PostgreSQL 실시간 동기화 관리자 + /// Phase 1: 기본 동기화 프레임워크 + /// Phase 2+: GameObject/Component 실시간 추적, 배치 업데이트 + /// + public class SyncManager : IDisposable + { + #region Fields + private readonly DatabaseManager databaseManager; + private bool isRunning = false; + private bool isDisposed = false; + private CancellationTokenSource syncCts; + + // 동기화 설정 + private const int SyncIntervalMilliseconds = 1000; // 1초마다 동기화 + private const int BatchSize = 500; // 배치당 최대 500개 객체 + #endregion + + #region Properties + /// + /// 동기화 실행 중 여부 + /// + public bool IsRunning => isRunning; + + /// + /// 마지막 동기화 시간 + /// + public DateTime LastSyncTime { get; private set; } + + /// + /// 동기화 성공 횟수 + /// + public int SuccessfulSyncCount { get; private set; } + + /// + /// 동기화 실패 횟수 + /// + public int FailedSyncCount { get; private set; } + #endregion + + #region Constructor + public SyncManager(DatabaseManager databaseManager) + { + this.databaseManager = databaseManager ?? throw new ArgumentNullException(nameof(databaseManager)); + LastSyncTime = DateTime.MinValue; + SuccessfulSyncCount = 0; + FailedSyncCount = 0; + + ToolkitLogger.Log("SyncManager", "생성 완료."); + } + #endregion + + #region Sync Control + /// + /// 동기화 시작 + /// + public void StartSync() + { + ThrowIfDisposed(); + + if (isRunning) + { + ToolkitLogger.LogWarning("SyncManager", "이미 동기화가 실행 중입니다."); + return; + } + + if (!databaseManager.IsInitialized || !databaseManager.IsConnected) + { + ToolkitLogger.LogError("SyncManager", "DatabaseManager가 초기화되지 않았거나 연결되지 않았습니다."); + return; + } + + ToolkitLogger.Log("SyncManager", "동기화 시작..."); + + syncCts = new CancellationTokenSource(); + isRunning = true; + + // 백그라운드 동기화 루프 시작 (UniTask) + RunSyncLoopAsync(syncCts.Token).Forget(); + } + + /// + /// 동기화 중지 + /// + public void StopSync() + { + ThrowIfDisposed(); + + if (!isRunning) + { + return; + } + + ToolkitLogger.Log("SyncManager", "동기화 중지 중..."); + + syncCts?.Cancel(); + syncCts?.Dispose(); + syncCts = null; + + isRunning = false; + + ToolkitLogger.Log("SyncManager", "동기화 중지 완료."); + } + #endregion + + #region Sync Loop + /// + /// 백그라운드 동기화 루프 (UniTask) + /// + private async UniTaskVoid RunSyncLoopAsync(CancellationToken cancellationToken) + { + ToolkitLogger.Log("SyncManager", "동기화 루프 시작."); + + try + { + while (!cancellationToken.IsCancellationRequested) + { + // 동기화 수행 + await PerformSyncAsync(cancellationToken); + + // 대기 (1초) + await UniTask.Delay(SyncIntervalMilliseconds, cancellationToken: cancellationToken); + } + } + catch (OperationCanceledException) + { + ToolkitLogger.Log("SyncManager", "동기화 루프가 취소되었습니다."); + } + catch (Exception ex) + { + ToolkitLogger.LogError("SyncManager", $" 동기화 루프 중 예외 발생: {ex.Message}\n{ex.StackTrace}"); + isRunning = false; + } + + ToolkitLogger.Log("SyncManager", "동기화 루프 종료."); + } + + /// + /// 단일 동기화 수행 (모든 로드된 씬) + /// + private async UniTask PerformSyncAsync(CancellationToken cancellationToken) + { + try + { + // Phase 1: 단순 연결 테스트만 수행 + // Phase 2: GameObject 변경 감지 및 배치 업데이트 + + bool isConnected = await databaseManager.TestConnectionAsync(); + if (!isConnected) + { + ToolkitLogger.LogWarning("SyncManager", "데이터베이스 연결이 끊어졌습니다."); + FailedSyncCount++; + return; + } + + // Phase 2: 모든 로드된 씬에 대해 GameObject 변경 감지 및 배치 업데이트 + int sceneCount = SceneManager.sceneCount; + if (sceneCount == 0) + { + ToolkitLogger.LogWarning("SyncManager", "로드된 씬이 없습니다."); + return; + } + + // 모든 로드된 씬 순회 + for (int i = 0; i < sceneCount; i++) + { + var scene = SceneManager.GetSceneAt(i); + + // 로드되지 않은 씬은 건너뛰기 + if (!scene.isLoaded) + { + continue; + } + + // 1. Unity Scene에서 모든 GameObject 수집 + var allGameObjects = CollectAllGameObjects(scene); + if (allGameObjects.Count == 0) + { + continue; // 빈 씬은 건너뛰기 + } + + // 2. DB에서 현재 씬의 GameObject 목록 가져오기 + Dictionary dbGameObjects; + try + { + dbGameObjects = await GetDatabaseGameObjectsAsync(scene, cancellationToken); + } + catch (InvalidOperationException ex) + { + ToolkitLogger.LogError("SyncManager", $" DB 연결 실패로 씬 '{scene.name}' 동기화 건너뜀: {ex.Message}"); + continue; // 이 씬은 건너뛰고 다음 씬 처리 + } + + // 3. 변경 감지 + var changes = DetectChanges(allGameObjects, dbGameObjects); + + // 4. 배치 업데이트 실행 + if (changes.Updated.Count > 0) + { + await BatchUpdateGameObjectsAsync(changes.Updated, cancellationToken); + } + + if (changes.Inserted.Count > 0) + { + await BatchInsertGameObjectsAsync(scene, changes.Inserted, cancellationToken); + } + + if (changes.Deleted.Count > 0) + { + await BatchMarkDeletedAsync(changes.Deleted, cancellationToken); + } + + // 취소 확인 + cancellationToken.ThrowIfCancellationRequested(); + } + + LastSyncTime = DateTime.UtcNow; + SuccessfulSyncCount++; + } + catch (OperationCanceledException) + { + throw; // 취소는 상위로 전파 + } + catch (Exception ex) + { + ToolkitLogger.LogError("SyncManager", $" 동기화 중 예외 발생: {ex.Message}"); + FailedSyncCount++; + } + } + #endregion + + #region Manual Sync + /// + /// 수동 동기화 (즉시 실행) + /// + public async UniTask SyncNowAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + if (!databaseManager.IsInitialized || !databaseManager.IsConnected) + { + return new SyncResult + { + Success = false, + ErrorMessage = "Database not initialized or not connected." + }; + } + + try + { + ToolkitLogger.Log("SyncManager", "수동 동기화 시작..."); + + await PerformSyncAsync(cancellationToken); + + ToolkitLogger.Log("SyncManager", "수동 동기화 완료."); + return new SyncResult + { + Success = true, + Message = "Manual sync completed successfully." + }; + } + catch (OperationCanceledException) + { + ToolkitLogger.LogWarning("SyncManager", "수동 동기화가 취소되었습니다."); + return new SyncResult + { + Success = false, + ErrorMessage = "Manual sync was canceled." + }; + } + catch (Exception ex) + { + ToolkitLogger.LogError("SyncManager", $" 수동 동기화 중 예외 발생: {ex.Message}"); + return new SyncResult + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + #endregion + + #region Batch Operations (Phase 2+) + /// + /// GameObject 배치 업데이트 + /// + /// 업데이트할 GameObject 목록 + public async UniTask BatchUpdateGameObjectsAsync(List gameObjects, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + if (gameObjects == null || gameObjects.Count == 0) + { + return 0; + } + + await UniTask.SwitchToThreadPool(); + + try + { + var connection = databaseManager.Connector?.Connection; + if (connection == null) + { + ToolkitLogger.LogError("SyncManager", "Database connection is null"); + return 0; + } + + int updatedCount = 0; + + // 배치 크기로 나누어 처리 + for (int i = 0; i < gameObjects.Count; i += BatchSize) + { + int batchCount = Math.Min(BatchSize, gameObjects.Count - i); + var batch = gameObjects.GetRange(i, batchCount); + + ExecuteInTransaction(connection, () => + { + foreach (var obj in batch) + { + var guidComp = EnsureGameObjectGuid(obj); + string guid = guidComp.Guid; + int instanceId = obj.GetInstanceID(); + int? parentId = obj.transform.parent != null ? obj.transform.parent.gameObject.GetInstanceID() : (int?)null; + + var sql = @" + UPDATE gameobjects + SET object_name = ?, + parent_id = ?, + tag = ?, + layer = ?, + is_active = ?, + is_static = ?, + instance_id = ?, + updated_at = datetime('now', 'localtime') + WHERE guid = ? + "; + + connection.Execute(sql, obj.name, parentId, obj.tag, obj.layer, obj.activeSelf, obj.isStatic, instanceId, guid); + } + }); + + updatedCount += batchCount; + ToolkitLogger.Log("SyncManager", $" 배치 업데이트 완료: {batchCount}개 GameObject"); + + // 취소 확인 + cancellationToken.ThrowIfCancellationRequested(); + } + + return updatedCount; + } + finally + { + // 메인 스레드가 살아있는지 확인 + if (!isDisposed) + { + await UniTask.SwitchToMainThread(); + } + } + } + + /// + /// Component 배치 업데이트 (Phase 2에서 구현) + /// + public async UniTask BatchUpdateComponentsAsync(List components, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + if (components == null || components.Count == 0) + { + return 0; + } + + // TODO Phase 2: Component 배치 업데이트 구현 + + await UniTask.Yield(); + return 0; + } + #endregion + + #region Helper Methods (Phase 2) + /// + /// 트랜잭션을 안전하게 실행하는 헬퍼 메서드 + /// + private void ExecuteInTransaction(SQLite.SQLiteConnection connection, Action action) + { + // SQLite는 중첩 트랜잭션을 지원하지 않으므로 try-catch로 감지 + bool transactionStarted = false; + try + { + connection.BeginTransaction(); + transactionStarted = true; + + action(); + + connection.Commit(); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("transaction") || ex.Message.Contains("Transaction")) + { + // 이미 트랜잭션이 시작된 경우, 그냥 액션만 실행 + ToolkitLogger.LogWarning("SyncManager", $" Transaction already started, executing without nested transaction: {ex.Message}"); + if (transactionStarted) + { + connection.Rollback(); + } + action(); + } + catch + { + if (transactionStarted) + { + connection.Rollback(); + } + throw; + } + } + + /// + /// Unity Scene에서 모든 GameObject 재귀적으로 수집 + /// + private List CollectAllGameObjects(Scene scene) + { + var result = new List(); + var rootObjects = scene.GetRootGameObjects(); + + foreach (var root in rootObjects) + { + CollectGameObjectsRecursive(root, result); + } + + return result; + } + + /// + /// GameObject를 재귀적으로 수집하는 헬퍼 메서드 + /// + private void CollectGameObjectsRecursive(GameObject obj, List list) + { + list.Add(obj); + for (int i = 0; i < obj.transform.childCount; i++) + { + CollectGameObjectsRecursive(obj.transform.GetChild(i).gameObject, list); + } + } + + /// + /// DB에서 현재 씬의 GameObject 목록 가져오기 (GUID 기반) + /// + private async UniTask> GetDatabaseGameObjectsAsync(Scene scene, CancellationToken cancellationToken) + { + await UniTask.SwitchToThreadPool(); + + try + { + var connection = databaseManager.Connector?.Connection; + if (connection == null) + { + ToolkitLogger.LogError("SyncManager", "Database connection is null"); + throw new InvalidOperationException("Database connection is not available"); + } + + // 1. scene_id 가져오기 + var sceneIdSql = "SELECT scene_id FROM scenes WHERE scene_path = ?"; + var sceneIds = connection.Query(sceneIdSql, scene.path); + + if (!sceneIds.Any()) + { + // Scene이 DB에 없는 것은 정상 (첫 동기화) + ToolkitLogger.Log("SyncManager", $" Scene '{scene.name}'이 DB에 없습니다 (첫 동기화)."); + return new Dictionary(); + } + + int sceneId = sceneIds.First().scene_id; + + // 2. GameObject 목록 가져오기 (guid 포함) + var sql = @" + SELECT object_id, instance_id, guid, object_name, parent_id, tag, layer, is_active, is_static, is_deleted + FROM gameobjects + WHERE scene_id = ? AND is_deleted = 0 + "; + + var dbObjects = connection.Query(sql, sceneId); + + // Dictionary로 변환 (guid를 키로 사용, guid가 null인 경우 instance_id를 fallback으로 사용) + var result = new Dictionary(); + foreach (var obj in dbObjects) + { + string key = !string.IsNullOrEmpty(obj.guid) ? obj.guid : $"instance_{obj.instance_id}"; + result[key] = obj; + } + + return result; + } + finally + { + // 메인 스레드가 살아있는지 확인 + if (!isDisposed) + { + await UniTask.SwitchToMainThread(); + } + } + } + + /// + /// Scene ID를 가져오기 위한 레코드 클래스 + /// + private class SceneIdRecord + { + public int scene_id { get; set; } + } + #endregion + + #region Change Detection (Phase 2) + /// + /// GameObject에 GameObjectGuid 컴포넌트 확보 (없으면 추가) + /// + private GameObjectGuid EnsureGameObjectGuid(GameObject obj) + { + var guidComp = obj.GetComponent(); + if (guidComp == null) + { + guidComp = obj.AddComponent(); + } + return guidComp; + } + + /// + /// Unity GameObject와 DB GameObject 비교하여 변경사항 감지 (GUID 기반) + /// + private GameObjectChangeSet DetectChanges(List unityObjects, Dictionary dbObjects) + { + var changeSet = new GameObjectChangeSet + { + Updated = new List(), + Inserted = new List(), + Deleted = new List() + }; + + // Unity에 있는 객체 확인 (GUID 기반) + var processedGuids = new HashSet(); + + foreach (var obj in unityObjects) + { + // GameObjectGuid 컴포넌트 확보 + var guidComp = EnsureGameObjectGuid(obj); + string guid = guidComp.Guid; + + processedGuids.Add(guid); + + if (dbObjects.TryGetValue(guid, out var dbObj)) + { + // DB에 존재: 변경 여부 확인 + if (HasChanged(obj, dbObj)) + { + changeSet.Updated.Add(obj); + } + } + else + { + // DB에 없음: 새로운 객체 + changeSet.Inserted.Add(obj); + } + } + + // DB에만 있고 Unity에 없는 객체 확인 (삭제된 객체) + foreach (var kvp in dbObjects) + { + if (!processedGuids.Contains(kvp.Key)) + { + changeSet.Deleted.Add(kvp.Value.object_id); + } + } + + return changeSet; + } + + /// + /// GameObject가 DB 레코드와 비교하여 변경되었는지 확인 + /// + private bool HasChanged(GameObject obj, DbGameObject dbObj) + { + // 이름 변경 + if (obj.name != dbObj.object_name) + return true; + + // Parent 변경 + int? currentParentId = obj.transform.parent != null ? obj.transform.parent.gameObject.GetInstanceID() : (int?)null; + if (currentParentId != dbObj.parent_id) + return true; + + // Tag 변경 + if (obj.tag != dbObj.tag) + return true; + + // Layer 변경 + if (obj.layer != dbObj.layer) + return true; + + // Active 상태 변경 + if (obj.activeSelf != dbObj.is_active) + return true; + + // Static 플래그 변경 + if (obj.isStatic != dbObj.is_static) + return true; + + return false; + } + #endregion + + #region Batch Insert/Delete (Phase 2) + /// + /// GameObject 배치 삽입 + /// + private async UniTask BatchInsertGameObjectsAsync(Scene scene, List gameObjects, CancellationToken cancellationToken) + { + if (gameObjects == null || gameObjects.Count == 0) + return; + + await UniTask.SwitchToThreadPool(); + + try + { + var connection = databaseManager.Connector?.Connection; + if (connection == null) + { + ToolkitLogger.LogError("SyncManager", "Database connection is null"); + return; + } + + // 1. scene_id 가져오기 (또는 생성) + int sceneId = EnsureSceneRecord(connection, scene); + + // 2. 배치 INSERT + for (int i = 0; i < gameObjects.Count; i += BatchSize) + { + int batchCount = Math.Min(BatchSize, gameObjects.Count - i); + var batch = gameObjects.GetRange(i, batchCount); + + ExecuteInTransaction(connection, () => + { + foreach (var obj in batch) + { + var guidComp = EnsureGameObjectGuid(obj); + string guid = guidComp.Guid; + int instanceId = obj.GetInstanceID(); + int? parentId = obj.transform.parent != null ? obj.transform.parent.gameObject.GetInstanceID() : (int?)null; + + var sql = @" + INSERT INTO gameobjects (guid, 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(sql, guid, instanceId, sceneId, obj.name, parentId, obj.tag, obj.layer, obj.activeSelf, obj.isStatic); + } + }); + + ToolkitLogger.Log("SyncManager", $" 배치 삽입 완료: {batchCount}개 GameObject"); + + cancellationToken.ThrowIfCancellationRequested(); + } + } + finally + { + // 메인 스레드가 살아있는지 확인 + if (!isDisposed) + { + await UniTask.SwitchToMainThread(); + } + } + } + + /// + /// GameObject 배치 삭제 (soft delete) + /// + private async UniTask BatchMarkDeletedAsync(List objectIds, CancellationToken cancellationToken) + { + if (objectIds == null || objectIds.Count == 0) + return; + + await UniTask.SwitchToThreadPool(); + + try + { + var connection = databaseManager.Connector?.Connection; + if (connection == null) + { + ToolkitLogger.LogError("SyncManager", "Database connection is null"); + return; + } + + // 배치 UPDATE (soft delete) + for (int i = 0; i < objectIds.Count; i += BatchSize) + { + int batchCount = Math.Min(BatchSize, objectIds.Count - i); + var batch = objectIds.GetRange(i, batchCount); + + ExecuteInTransaction(connection, () => + { + foreach (var objectId in batch) + { + var sql = @" + UPDATE gameobjects + SET is_deleted = 1, updated_at = datetime('now', 'localtime') + WHERE object_id = ? + "; + + connection.Execute(sql, objectId); + } + }); + + ToolkitLogger.Log("SyncManager", $" 배치 삭제 완료: {batchCount}개 GameObject"); + + cancellationToken.ThrowIfCancellationRequested(); + } + } + finally + { + // 메인 스레드가 살아있는지 확인 + if (!isDisposed) + { + await UniTask.SwitchToMainThread(); + } + } + } + + /// + /// Scene 레코드가 DB에 존재하는지 확인하고 없으면 생성 + /// + private int EnsureSceneRecord(SQLite.SQLiteConnection connection, Scene scene) + { + var sceneIdSql = "SELECT scene_id FROM scenes WHERE scene_path = ?"; + var sceneIds = connection.Query(sceneIdSql, scene.path); + + if (sceneIds.Any()) + { + return sceneIds.First().scene_id; + } + + // Scene 레코드 생성 + var insertSql = @" + INSERT INTO scenes (scene_path, scene_name, created_at, updated_at) + VALUES (?, ?, datetime('now', 'localtime'), datetime('now', 'localtime')) + "; + connection.Execute(insertSql, scene.path, scene.name); + + // 생성된 scene_id 반환 + var newSceneIds = connection.Query(sceneIdSql, scene.path); + return newSceneIds.First().scene_id; + } + #endregion + + #region Health Check + /// + /// SyncManager 상태 정보 + /// + public SyncHealthStatus GetHealthStatus() + { + return new SyncHealthStatus + { + IsRunning = isRunning, + LastSyncTime = LastSyncTime, + SuccessfulSyncCount = SuccessfulSyncCount, + FailedSyncCount = FailedSyncCount, + SyncIntervalMs = SyncIntervalMilliseconds, + BatchSize = BatchSize + }; + } + #endregion + + #region Disposal + public void Dispose() + { + if (isDisposed) + { + return; + } + + StopSync(); + isDisposed = true; + + ToolkitLogger.Log("SyncManager", "Disposed."); + } + + private void ThrowIfDisposed() + { + if (isDisposed) + { + throw new ObjectDisposedException(nameof(SyncManager)); + } + } + #endregion + } + + #region Result Structs + /// + /// 동기화 결과 + /// + public struct SyncResult + { + public bool Success; + public string Message; + public string ErrorMessage; + } + + /// + /// SyncManager 상태 + /// + public struct SyncHealthStatus + { + public bool IsRunning; + public DateTime LastSyncTime; + public int SuccessfulSyncCount; + public int FailedSyncCount; + public int SyncIntervalMs; + public int BatchSize; + + public override string ToString() + { + return $"[SyncHealthStatus]\n" + + $" Running: {IsRunning}\n" + + $" Last Sync: {LastSyncTime:yyyy-MM-dd HH:mm:ss}\n" + + $" Success: {SuccessfulSyncCount}, Failed: {FailedSyncCount}\n" + + $" Interval: {SyncIntervalMs}ms, Batch: {BatchSize}"; + } + } + + /// + /// DB GameObject 레코드 (SQLite-net ORM용) + /// + public class DbGameObject + { + public int object_id { get; set; } + public int instance_id { get; set; } + public string guid { get; set; } + public string object_name { get; set; } + public int? parent_id { get; set; } + public string tag { get; set; } + public int layer { get; set; } + public bool is_active { get; set; } + public bool is_static { get; set; } + public bool is_deleted { get; set; } + } + + /// + /// GameObject 변경사항 집합 + /// + public class GameObjectChangeSet + { + public List Updated { get; set; } + public List Inserted { get; set; } + public List Deleted { get; set; } + } + #endregion +} diff --git a/skills/assets/unity-package/Editor/Database/SyncManager.cs.meta b/skills/assets/unity-package/Editor/Database/SyncManager.cs.meta new file mode 100644 index 0000000..e27cc56 --- /dev/null +++ b/skills/assets/unity-package/Editor/Database/SyncManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 97a6b4ca15448704ab5758aa7642bca3 \ No newline at end of file diff --git a/skills/assets/unity-package/Editor/DatabaseStatusWindow.cs b/skills/assets/unity-package/Editor/DatabaseStatusWindow.cs new file mode 100644 index 0000000..8a6644b --- /dev/null +++ b/skills/assets/unity-package/Editor/DatabaseStatusWindow.cs @@ -0,0 +1,371 @@ +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; +using UnityEditorToolkit.Editor.Utils; + +namespace UnityEditorToolkit.Editor +{ + /// + /// Database 상태 및 컨트롤을 표시하는 별도 윈도우 + /// + public class DatabaseStatusWindow : EditorWindow + { + #region Fields + private EditorServerWindow parentWindow; + + // Data binding source + private EditorServerWindowData windowData = new EditorServerWindowData(); + + // UI Elements - Status + private VisualElement dbStatusIndicator; + private Label dbStatusLabel; + private Label dbFileExistsLabel; + private Label dbSyncStatusLabel; + + // UI Elements - Buttons + private Button dbTestButton; + private Button dbConnectButton; + private Button dbDisconnectButton; + private Button dbMigrateButton; + private Button dbSyncToggleButton; + + // UI Elements - Command History + private Label dbUndoCount; + private Label dbRedoCount; + private Button dbUndoButton; + private Button dbRedoButton; + private Button dbClearHistoryButton; + + // UI Elements - Messages + private HelpBox dbErrorHelp; + private HelpBox dbSuccessHelp; + #endregion + + #region Window Management + /// + /// 윈도우 열기 (팩토리 메서드) + /// + public static DatabaseStatusWindow Open(EditorServerWindow parentWindow) + { + ToolkitLogger.LogDebug("DatabaseStatusWindow", $"Open 시작, parentWindow: {(parentWindow != null ? "존재" : "null")}"); + + var window = GetWindow("Database Status & Controls"); + window.minSize = new Vector2(400, 500); + + ToolkitLogger.LogDebug("DatabaseStatusWindow", $"GetWindow 완료, parentWindow 설정 전: {(window.parentWindow != null ? "존재" : "null")}"); + + window.parentWindow = parentWindow; + + ToolkitLogger.LogDebug("DatabaseStatusWindow", $"parentWindow 설정 완료: {(window.parentWindow != null ? "존재" : "null")}"); + + window.Show(); + + // CreateGUI()가 parentWindow 설정 전에 실행되었을 수 있으므로 다시 업데이트 + ToolkitLogger.LogDebug("DatabaseStatusWindow", "Open에서 UpdateUI() 호출"); + window.UpdateUI(); + + return window; + } + #endregion + + #region Unity Lifecycle + private void CreateGUI() + { + ToolkitLogger.LogDebug("DatabaseStatusWindow", $"CreateGUI 시작, parentWindow: {(parentWindow != null ? "존재" : "null")}"); + + // Load UXML + var visualTree = AssetDatabase.LoadAssetAtPath( + "Packages/com.devgom.unity-editor-toolkit/Editor/DatabaseStatusWindow.uxml"); + + if (visualTree == null) + { + ToolkitLogger.LogError("DatabaseStatusWindow", "UXML file not found!"); + return; + } + + visualTree.CloneTree(rootVisualElement); + + // Set data binding source + rootVisualElement.dataSource = windowData; + + // Load USS (EditorServerWindow.uss 재사용) + var styleSheet = AssetDatabase.LoadAssetAtPath( + "Packages/com.devgom.unity-editor-toolkit/Editor/EditorServerWindow.uss"); + + if (styleSheet != null) + { + rootVisualElement.styleSheets.Add(styleSheet); + } + + // Query UI elements + QueryUIElements(); + + // Register events + RegisterEvents(); + + // Initial UI update + ToolkitLogger.LogDebug("DatabaseStatusWindow", "CreateGUI에서 UpdateUI() 호출"); + UpdateUI(); + } + + private void OnDestroy() + { + // Cleanup + UnregisterEvents(); + } + #endregion + + #region UI Query + private void QueryUIElements() + { + var root = rootVisualElement; + + // Status + dbStatusIndicator = root.Q("db-status-indicator"); + dbStatusLabel = root.Q