@atelierai/uco 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +7 -8
- package/bin/server.mjs +1 -1
- package/bin/stub-plugin.mjs +3 -3
- package/dist/codegen/coerce.js +1 -1
- package/dist/codegen/coerce.js.map +1 -1
- package/dist/codegen/emit.js +2 -2
- package/dist/codegen/emit.js.map +1 -1
- package/dist/codegen/refs.js +1 -1
- package/dist/codegen/refs.js.map +1 -1
- package/dist/codegen/types.js +1 -1
- package/dist/codegen/types.js.map +1 -1
- package/dist/commands/call.js +1 -1
- package/dist/commands/call.js.map +1 -1
- package/dist/commands/devops/configure.js +1 -1
- package/dist/commands/devops/configure.js.map +1 -1
- package/dist/commands/devops/index.js +0 -4
- package/dist/commands/devops/index.js.map +1 -1
- package/dist/commands/devops/install-plugin.js +1 -1
- package/dist/commands/devops/install-plugin.js.map +1 -1
- package/dist/commands/devops/open.js +7 -7
- package/dist/commands/devops/open.js.map +1 -1
- package/dist/commands/devops/status.js +3 -3
- package/dist/commands/devops/status.js.map +1 -1
- package/dist/commands/exec.js +1 -1
- package/dist/commands/exec.js.map +1 -1
- package/dist/commands/install.js +2 -2
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/prompt.js +2 -2
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/resource.js +2 -2
- package/dist/commands/resource.js.map +1 -1
- package/dist/commands/update.js +1 -26
- package/dist/commands/update.js.map +1 -1
- package/dist/config/port.js +2 -2
- package/dist/config/port.js.map +1 -1
- package/dist/config/resolve.d.ts +1 -2
- package/dist/config/resolve.js +0 -12
- package/dist/config/resolve.js.map +1 -1
- package/dist/devops/lib/configure.d.ts +1 -1
- package/dist/devops/lib/configure.js +2 -2
- package/dist/devops/lib/configure.js.map +1 -1
- package/dist/devops/lib/install-plugin.d.ts +1 -1
- package/dist/devops/lib/install-plugin.js +1 -1
- package/dist/devops/lib/install-plugin.js.map +1 -1
- package/dist/devops/lib/install.d.ts +2 -2
- package/dist/devops/lib/install.js +5 -5
- package/dist/devops/lib/install.js.map +1 -1
- package/dist/devops/lib/open.js +11 -32
- package/dist/devops/lib/open.js.map +1 -1
- package/dist/devops/lib/remove-plugin.d.ts +1 -1
- package/dist/devops/lib/remove-plugin.js +3 -3
- package/dist/devops/lib/remove-plugin.js.map +1 -1
- package/dist/devops/lib/run-tool.d.ts +2 -2
- package/dist/devops/lib/run-tool.js +2 -2
- package/dist/devops/lib/types.d.ts +17 -54
- package/dist/devops/lib/types.js +2 -2
- package/dist/devops/lib/types.js.map +1 -1
- package/dist/devops/lib/update.d.ts +0 -15
- package/dist/devops/lib/update.js +2 -91
- package/dist/devops/lib/update.js.map +1 -1
- package/dist/devops/utils/agents.d.ts +0 -28
- package/dist/devops/utils/agents.js +0 -462
- package/dist/devops/utils/agents.js.map +1 -1
- package/dist/devops/utils/config.d.ts +9 -18
- package/dist/devops/utils/config.js +7 -20
- package/dist/devops/utils/config.js.map +1 -1
- package/dist/devops/utils/editor-cache.js +1 -1
- package/dist/devops/utils/editor-cache.js.map +1 -1
- package/dist/devops/utils/launch-error-dismiss.d.ts +2 -2
- package/dist/devops/utils/launch-error-dismiss.js +5 -5
- package/dist/devops/utils/manifest.d.ts +2 -2
- package/dist/devops/utils/manifest.js +3 -3
- package/dist/devops/utils/manifest.js.map +1 -1
- package/dist/devops/utils/port.d.ts +1 -1
- package/dist/devops/utils/port.js +1 -1
- package/dist/devops/utils/probe.d.ts +1 -1
- package/dist/devops/utils/probe.js +1 -1
- package/dist/devops/utils/ui.js +2 -2
- package/dist/devops/utils/ui.js.map +1 -1
- package/dist/devops/utils/unity-editor.js +4 -4
- package/dist/devops/utils/unity-editor.js.map +1 -1
- package/dist/devops/utils/unity-hub.js +1 -1
- package/dist/devops/utils/unity-hub.js.map +1 -1
- package/dist/devops/utils/unity-shutdown.d.ts +1 -1
- package/dist/devops/utils/unity-shutdown.js +1 -1
- package/dist/devops/utils/update-check.js +3 -3
- package/dist/devops/utils/update-check.js.map +1 -1
- package/dist/devops/utils/vendor.d.ts +2 -2
- package/dist/devops/utils/vendor.js +4 -4
- package/dist/devops/utils/vendor.js.map +1 -1
- package/dist/generated/tools.js +1 -1
- package/dist/generated/tools.js.map +1 -1
- package/dist/program.js +3 -3
- package/dist/program.js.map +1 -1
- package/dist/server/app.d.ts +5 -5
- package/dist/server/app.js +3 -3
- package/dist/server/app.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/rest/auth.d.ts +1 -1
- package/dist/server/rest/auth.js +3 -3
- package/dist/server/rest/auth.js.map +1 -1
- package/dist/server/rest/context.d.ts +2 -2
- package/dist/server/rest/forward.d.ts +2 -2
- package/dist/server/session/store.d.ts +3 -3
- package/dist/server/session/store.js +4 -4
- package/dist/server/session/store.js.map +1 -1
- package/dist/server/stub-client/scenarios.js +8 -8
- package/dist/server/stub-client/scenarios.js.map +1 -1
- package/dist/server/types.d.ts +11 -11
- package/dist/server/types.js +9 -9
- package/dist/server/types.js.map +1 -1
- package/dist/server/ws/hub.d.ts +8 -8
- package/dist/server/ws/hub.js +13 -13
- package/dist/server/ws/hub.js.map +1 -1
- package/dist/server/ws/rpc.d.ts +3 -3
- package/dist/server/ws/rpc.js +4 -4
- package/dist/server/ws/rpc.js.map +1 -1
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/index.js +3 -3
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/rest.js +1 -1
- package/dist/transport/rest.js.map +1 -1
- package/docs/INSTALL.zh-CN.md +2 -3
- package/package.json +1 -1
- package/skills/uco-setup/SKILL.md +1 -1
- package/skills/unity-editor/references/diagnostics.md +24 -24
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +1034 -1034
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Components/ExtensionPanel.cs +379 -379
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/MenuItems.cs +72 -72
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/UnityCopilotProjectSettingsProvider.cs +64 -64
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +372 -372
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
- package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
- package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
- package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +175 -175
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +368 -368
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +4 -4
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
- package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
- package/dist/commands/devops/login.d.ts +0 -2
- package/dist/commands/devops/login.js +0 -29
- package/dist/commands/devops/login.js.map +0 -1
- package/dist/commands/devops/setup-mcp.d.ts +0 -2
- package/dist/commands/devops/setup-mcp.js +0 -41
- package/dist/commands/devops/setup-mcp.js.map +0 -1
- package/dist/devops/commands-upstream/bootstrap-local.d.ts +0 -17
- package/dist/devops/commands-upstream/bootstrap-local.js +0 -87
- package/dist/devops/commands-upstream/bootstrap-local.js.map +0 -1
- package/dist/devops/commands-upstream/close.d.ts +0 -57
- package/dist/devops/commands-upstream/close.js +0 -188
- package/dist/devops/commands-upstream/close.js.map +0 -1
- package/dist/devops/commands-upstream/configure.d.ts +0 -2
- package/dist/devops/commands-upstream/configure.js +0 -87
- package/dist/devops/commands-upstream/configure.js.map +0 -1
- package/dist/devops/commands-upstream/create-project.d.ts +0 -2
- package/dist/devops/commands-upstream/create-project.js +0 -43
- package/dist/devops/commands-upstream/create-project.js.map +0 -1
- package/dist/devops/commands-upstream/install-plugin.d.ts +0 -2
- package/dist/devops/commands-upstream/install-plugin.js +0 -64
- package/dist/devops/commands-upstream/install-plugin.js.map +0 -1
- package/dist/devops/commands-upstream/install-unity.d.ts +0 -2
- package/dist/devops/commands-upstream/install-unity.js +0 -72
- package/dist/devops/commands-upstream/install-unity.js.map +0 -1
- package/dist/devops/commands-upstream/login.d.ts +0 -2
- package/dist/devops/commands-upstream/login.js +0 -33
- package/dist/devops/commands-upstream/login.js.map +0 -1
- package/dist/devops/commands-upstream/open.d.ts +0 -32
- package/dist/devops/commands-upstream/open.js +0 -282
- package/dist/devops/commands-upstream/open.js.map +0 -1
- package/dist/devops/commands-upstream/remove-plugin.d.ts +0 -2
- package/dist/devops/commands-upstream/remove-plugin.js +0 -41
- package/dist/devops/commands-upstream/remove-plugin.js.map +0 -1
- package/dist/devops/commands-upstream/run-system-tool.d.ts +0 -1
- package/dist/devops/commands-upstream/run-system-tool.js +0 -13
- package/dist/devops/commands-upstream/run-system-tool.js.map +0 -1
- package/dist/devops/commands-upstream/run-tool-builder.d.ts +0 -25
- package/dist/devops/commands-upstream/run-tool-builder.js +0 -125
- package/dist/devops/commands-upstream/run-tool-builder.js.map +0 -1
- package/dist/devops/commands-upstream/run-tool.d.ts +0 -1
- package/dist/devops/commands-upstream/run-tool.js +0 -13
- package/dist/devops/commands-upstream/run-tool.js.map +0 -1
- package/dist/devops/commands-upstream/setup-mcp.d.ts +0 -2
- package/dist/devops/commands-upstream/setup-mcp.js +0 -68
- package/dist/devops/commands-upstream/setup-mcp.js.map +0 -1
- package/dist/devops/commands-upstream/setup-skills.d.ts +0 -2
- package/dist/devops/commands-upstream/setup-skills.js +0 -167
- package/dist/devops/commands-upstream/setup-skills.js.map +0 -1
- package/dist/devops/commands-upstream/status.d.ts +0 -2
- package/dist/devops/commands-upstream/status.js +0 -87
- package/dist/devops/commands-upstream/status.js.map +0 -1
- package/dist/devops/commands-upstream/update.d.ts +0 -2
- package/dist/devops/commands-upstream/update.js +0 -77
- package/dist/devops/commands-upstream/update.js.map +0 -1
- package/dist/devops/commands-upstream/wait-for-ready.d.ts +0 -2
- package/dist/devops/commands-upstream/wait-for-ready.js +0 -93
- package/dist/devops/commands-upstream/wait-for-ready.js.map +0 -1
- package/dist/devops/lib/setup-mcp.d.ts +0 -35
- package/dist/devops/lib/setup-mcp.js +0 -177
- package/dist/devops/lib/setup-mcp.js.map +0 -1
- package/dist/devops/utils/cloud-login.d.ts +0 -7
- package/dist/devops/utils/cloud-login.js +0 -56
- package/dist/devops/utils/cloud-login.js.map +0 -1
- /package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs.meta → UcoPluginTests.cs.meta} +0 -0
package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs
CHANGED
|
@@ -1,1467 +1,1467 @@
|
|
|
1
|
-
/*
|
|
2
|
-
┌──────────────────────────────────────────────────────────────────┐
|
|
3
|
-
│ Author: AtelierAI │
|
|
4
|
-
│ Copyright (c) 2025 AtelierAI │
|
|
5
|
-
│ Licensed under the MIT License. │
|
|
6
|
-
└──────────────────────────────────────────────────────────────────┘
|
|
7
|
-
*/
|
|
8
|
-
#nullable enable
|
|
9
|
-
|
|
10
|
-
using System;
|
|
11
|
-
using System.Collections.Generic;
|
|
12
|
-
using System.Linq;
|
|
13
|
-
using System.Text.Json;
|
|
14
|
-
using AIGD;
|
|
15
|
-
using com.AtelierAI.Uco.Framework;
|
|
16
|
-
using com.AtelierAI.Uco.Framework.Common.Model;
|
|
17
|
-
using com.IvanMurzak.ReflectorNet.Utils;
|
|
18
|
-
using com.AtelierAI.Unity.Copilot.Editor.API;
|
|
19
|
-
using com.AtelierAI.Unity.Copilot.Runtime.Extensions;
|
|
20
|
-
using com.AtelierAI.Unity.Copilot.Runtime.Utils;
|
|
21
|
-
using UnityEditor;
|
|
22
|
-
using UnityEditor.SceneManagement;
|
|
23
|
-
using UnityEngine;
|
|
24
|
-
|
|
25
|
-
namespace com.AtelierAI.Unity.Copilot.Editor.Utils
|
|
26
|
-
{
|
|
27
|
-
/// <summary>
|
|
28
|
-
/// Read-only validator shared by the Scene/GameObject/Component pilot.
|
|
29
|
-
/// It resolves the real Unity targets (objects, components, scenes,
|
|
30
|
-
/// scene assets, and canonical paths) on the main thread without
|
|
31
|
-
/// creating, modifying, destroying, dirtying, or saving anything. The
|
|
32
|
-
/// reflected runner is deliberately never called from this adapter.
|
|
33
|
-
/// </summary>
|
|
34
|
-
public sealed class UnityPilotAuthoringValidator : IAuthoringValidator
|
|
35
|
-
{
|
|
36
|
-
public AuthoringValidationResult Validate(AuthoringInvocation invocation)
|
|
37
|
-
{
|
|
38
|
-
if (invocation == null)
|
|
39
|
-
throw new ArgumentNullException(nameof(invocation));
|
|
40
|
-
|
|
41
|
-
var inspection = UnityPilotAuthoringInspection.Inspect(invocation);
|
|
42
|
-
return new AuthoringValidationResult
|
|
43
|
-
{
|
|
44
|
-
Valid = inspection.Errors.Count == 0,
|
|
45
|
-
FailureCode = inspection.Errors.Count == 0 ? null : "validation_failed",
|
|
46
|
-
Targets = inspection.Targets.Take(32).ToArray(),
|
|
47
|
-
Details = inspection.Errors.Count == 0
|
|
48
|
-
? null
|
|
49
|
-
: new System.Text.Json.Nodes.JsonObject
|
|
50
|
-
{
|
|
51
|
-
["reason"] = inspection.Errors[0],
|
|
52
|
-
["errorCount"] = inspection.Errors.Count,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/// <summary>
|
|
59
|
-
/// Result of one read-only pilot inspection. Targets carry a state
|
|
60
|
-
/// fingerprint derived from the live Unity object (identity, hierarchy
|
|
61
|
-
/// path, owning scene/stage, component set) so a later change to the
|
|
62
|
-
/// inspected target makes an issued confirmation plan stale.
|
|
63
|
-
/// </summary>
|
|
64
|
-
internal sealed class UnityPilotInspection
|
|
65
|
-
{
|
|
66
|
-
public List<AuthoringTargetSummary> Targets { get; } = new List<AuthoringTargetSummary>();
|
|
67
|
-
public List<string> Errors { get; } = new List<string>();
|
|
68
|
-
public List<string> Effects { get; } = new List<string>();
|
|
69
|
-
public System.Text.Json.Nodes.JsonObject Before { get; } = new System.Text.Json.Nodes.JsonObject();
|
|
70
|
-
public System.Text.Json.Nodes.JsonObject After { get; } = new System.Text.Json.Nodes.JsonObject();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/// <summary>
|
|
74
|
-
/// Main-thread, read-only inspection of the pilot tools' targets. Every
|
|
75
|
-
/// lookup uses the same resolution helpers the runners use
|
|
76
|
-
/// (<c>GameObjectUtils.FindBy</c>, <c>FindAssetObject</c>,
|
|
77
|
-
/// <c>ComponentRef.Matches</c>), but no Unity API with a side effect is
|
|
78
|
-
/// reachable from this type.
|
|
79
|
-
/// </summary>
|
|
80
|
-
internal static class UnityPilotAuthoringInspection
|
|
81
|
-
{
|
|
82
|
-
private const int MaxListedTargets = 32;
|
|
83
|
-
|
|
84
|
-
public static UnityPilotInspection Inspect(AuthoringInvocation invocation)
|
|
85
|
-
=> MainThread.Instance.Run(() => InspectOnMainThread(invocation));
|
|
86
|
-
|
|
87
|
-
private static UnityPilotInspection InspectOnMainThread(AuthoringInvocation invocation)
|
|
88
|
-
{
|
|
89
|
-
var result = new UnityPilotInspection();
|
|
90
|
-
foreach (var binding in invocation.PathBindings.Values)
|
|
91
|
-
{
|
|
92
|
-
if (binding.Resolution.IsReadOnly
|
|
93
|
-
&& binding.Resolution.Intent != ProjectPathAccessIntent.Read)
|
|
94
|
-
result.Errors.Add("path_read_only");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
switch (invocation.Name)
|
|
98
|
-
{
|
|
99
|
-
case Tool_GameObject.GameObjectCreateToolId:
|
|
100
|
-
InspectGameObjectCreate(invocation, result);
|
|
101
|
-
break;
|
|
102
|
-
case Tool_GameObject.GameObjectDestroyToolId:
|
|
103
|
-
InspectSingleGameObject(invocation, result, "gameObjectRef", "destroy");
|
|
104
|
-
break;
|
|
105
|
-
case Tool_GameObject.GameObjectModifyToolId:
|
|
106
|
-
InspectGameObjectList(invocation, result, "modify");
|
|
107
|
-
break;
|
|
108
|
-
case Tool_GameObject.GameObjectDuplicateToolId:
|
|
109
|
-
InspectGameObjectList(invocation, result, "duplicate");
|
|
110
|
-
break;
|
|
111
|
-
case Tool_GameObject.GameObjectSetParentToolId:
|
|
112
|
-
InspectGameObjectList(invocation, result, "parent");
|
|
113
|
-
InspectOptionalOrRequiredGameObject(invocation, result, "parentGameObjectRef", required: true, kind: "Parent");
|
|
114
|
-
break;
|
|
115
|
-
case Tool_GameObject.GameObjectComponentAddToolId:
|
|
116
|
-
InspectComponentAdd(invocation, result);
|
|
117
|
-
break;
|
|
118
|
-
case Tool_GameObject.GameObjectComponentModifyToolId:
|
|
119
|
-
InspectComponentModify(invocation, result);
|
|
120
|
-
break;
|
|
121
|
-
case Tool_GameObject.GameObjectComponentDestroyToolId:
|
|
122
|
-
InspectComponentDestroy(invocation, result);
|
|
123
|
-
break;
|
|
124
|
-
case Tool_Scene.SceneOpenToolId:
|
|
125
|
-
InspectSceneAsset(invocation, result, requireOpened: false, effect: "open scene");
|
|
126
|
-
break;
|
|
127
|
-
case Tool_Scene.SceneSetActiveToolId:
|
|
128
|
-
InspectSceneAsset(invocation, result, requireOpened: true, effect: "set active scene");
|
|
129
|
-
break;
|
|
130
|
-
case Tool_Scene.SceneCreateToolId:
|
|
131
|
-
InspectSceneCreate(invocation, result);
|
|
132
|
-
break;
|
|
133
|
-
case Tool_Scene.SceneSaveToolId:
|
|
134
|
-
InspectSceneSave(invocation, result);
|
|
135
|
-
break;
|
|
136
|
-
case Tool_Scene.SceneUnloadToolId:
|
|
137
|
-
InspectSceneUnload(invocation, result);
|
|
138
|
-
break;
|
|
139
|
-
default:
|
|
140
|
-
InspectArgumentTargets(invocation, result);
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
result.Before["targetCount"] = result.Targets.Count;
|
|
145
|
-
result.Before["activeScene"] = SafeSceneDisplay(EditorSceneManager.GetActiveScene());
|
|
146
|
-
result.After["targetCount"] = result.Targets.Count;
|
|
147
|
-
result.After["predictedEffects"] = result.Effects.Take(32).Aggregate(
|
|
148
|
-
new System.Text.Json.Nodes.JsonArray(),
|
|
149
|
-
(array, effect) =>
|
|
150
|
-
{
|
|
151
|
-
array.Add(effect);
|
|
152
|
-
return array;
|
|
153
|
-
});
|
|
154
|
-
return result;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private static void InspectGameObjectCreate(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
158
|
-
{
|
|
159
|
-
TryGetString(invocation.Arguments, "name", out var name);
|
|
160
|
-
if (string.IsNullOrEmpty(name))
|
|
161
|
-
result.Errors.Add("name_missing");
|
|
162
|
-
|
|
163
|
-
var parentFingerprint = string.Empty;
|
|
164
|
-
if (TryGetArgument(invocation.Arguments, "parentGameObjectRef", out var parentElement)
|
|
165
|
-
&& parentElement.ValueKind == JsonValueKind.Object)
|
|
166
|
-
{
|
|
167
|
-
var parentRef = Deserialize<GameObjectRef>(parentElement);
|
|
168
|
-
if (parentRef != null && parentRef.IsValid(out _))
|
|
169
|
-
{
|
|
170
|
-
var parent = GameObjectUtils.FindBy(parentRef, out var error);
|
|
171
|
-
if (parent == null)
|
|
172
|
-
{
|
|
173
|
-
result.Errors.Add("parent_not_found");
|
|
174
|
-
}
|
|
175
|
-
else
|
|
176
|
-
{
|
|
177
|
-
var target = GameObjectTarget(parent, "Parent");
|
|
178
|
-
parentFingerprint = target.Fingerprint ?? string.Empty;
|
|
179
|
-
result.Targets.Add(target);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
var container = ContainerFingerprint();
|
|
185
|
-
result.Targets.Add(new AuthoringTargetSummary
|
|
186
|
-
{
|
|
187
|
-
Kind = "GameObject",
|
|
188
|
-
Name = Bound(name),
|
|
189
|
-
Fingerprint = Hash("create|" + name + "|" + container + "|" + parentFingerprint),
|
|
190
|
-
});
|
|
191
|
-
result.Effects.Add("create GameObject '" + Bound(name) + "'");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private static void InspectSingleGameObject(
|
|
195
|
-
AuthoringInvocation invocation,
|
|
196
|
-
UnityPilotInspection result,
|
|
197
|
-
string argumentName,
|
|
198
|
-
string effect)
|
|
199
|
-
{
|
|
200
|
-
var go = ResolveRequiredGameObject(invocation, result, argumentName, "GameObject");
|
|
201
|
-
if (go == null)
|
|
202
|
-
return;
|
|
203
|
-
result.Effects.Add(effect + " GameObject '" + Bound(go.name) + "' (" + go.transform.childCount + " children)");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private static void InspectOptionalOrRequiredGameObject(
|
|
207
|
-
AuthoringInvocation invocation,
|
|
208
|
-
UnityPilotInspection result,
|
|
209
|
-
string argumentName,
|
|
210
|
-
bool required,
|
|
211
|
-
string kind)
|
|
212
|
-
{
|
|
213
|
-
if (!TryGetArgument(invocation.Arguments, argumentName, out var element)
|
|
214
|
-
|| element.ValueKind != JsonValueKind.Object)
|
|
215
|
-
{
|
|
216
|
-
if (required)
|
|
217
|
-
result.Errors.Add("target_missing");
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
var reference = Deserialize<GameObjectRef>(element);
|
|
222
|
-
if (reference == null || !reference.IsValid(out _))
|
|
223
|
-
{
|
|
224
|
-
if (required)
|
|
225
|
-
result.Errors.Add("target_invalid");
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
var go = GameObjectUtils.FindBy(reference, out _);
|
|
230
|
-
if (go == null)
|
|
231
|
-
{
|
|
232
|
-
result.Errors.Add("target_not_found");
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
result.Targets.Add(GameObjectTarget(go, kind));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private static void InspectGameObjectList(
|
|
239
|
-
AuthoringInvocation invocation,
|
|
240
|
-
UnityPilotInspection result,
|
|
241
|
-
string effect)
|
|
242
|
-
{
|
|
243
|
-
if (!TryGetArgument(invocation.Arguments, "gameObjectRefs", out var element)
|
|
244
|
-
|| element.ValueKind != JsonValueKind.Array)
|
|
245
|
-
{
|
|
246
|
-
result.Errors.Add("target_missing");
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
var list = Deserialize<GameObjectRefList>(element);
|
|
251
|
-
if (list == null || list.Count == 0)
|
|
252
|
-
{
|
|
253
|
-
result.Errors.Add("target_missing");
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
var resolved = 0;
|
|
258
|
-
foreach (var reference in list)
|
|
259
|
-
{
|
|
260
|
-
if (reference == null || !reference.IsValid(out _))
|
|
261
|
-
{
|
|
262
|
-
result.Errors.Add("target_invalid");
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
var go = GameObjectUtils.FindBy(reference, out _);
|
|
266
|
-
if (go == null)
|
|
267
|
-
{
|
|
268
|
-
result.Errors.Add("target_not_found");
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
resolved++;
|
|
272
|
-
if (result.Targets.Count < MaxListedTargets)
|
|
273
|
-
result.Targets.Add(GameObjectTarget(go, "GameObject"));
|
|
274
|
-
}
|
|
275
|
-
result.Effects.Add(effect + " " + resolved + " GameObject(s)");
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
private static void InspectComponentAdd(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
279
|
-
{
|
|
280
|
-
var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
|
|
281
|
-
if (!TryGetArgument(invocation.Arguments, "componentNames", out var namesElement)
|
|
282
|
-
|| namesElement.ValueKind != JsonValueKind.Array)
|
|
283
|
-
{
|
|
284
|
-
result.Errors.Add("component_names_missing");
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
var count = 0;
|
|
289
|
-
foreach (var item in namesElement.EnumerateArray())
|
|
290
|
-
{
|
|
291
|
-
if (item.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetString()))
|
|
292
|
-
{
|
|
293
|
-
result.Errors.Add("component_name_invalid");
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
var componentName = item.GetString()!;
|
|
298
|
-
var type = TypeUtils.GetType(componentName)
|
|
299
|
-
?? Tool_GameObject.AllComponentTypes.FirstOrDefault(t => t.Name == componentName);
|
|
300
|
-
if (type == null)
|
|
301
|
-
{
|
|
302
|
-
result.Errors.Add("component_type_not_found");
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (!typeof(UnityEngine.Component).IsAssignableFrom(type))
|
|
306
|
-
{
|
|
307
|
-
result.Errors.Add("component_type_not_component");
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
count++;
|
|
311
|
-
result.Targets.Add(new AuthoringTargetSummary
|
|
312
|
-
{
|
|
313
|
-
Kind = "ComponentType",
|
|
314
|
-
Name = Bound(type.FullName),
|
|
315
|
-
Fingerprint = Hash("component-type|" + type.AssemblyQualifiedName),
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (count == 0 && result.Errors.Count == 0)
|
|
320
|
-
result.Errors.Add("component_names_missing");
|
|
321
|
-
if (go != null)
|
|
322
|
-
result.Effects.Add("add " + count + " component(s) to GameObject '" + Bound(go.name) + "'");
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private static void InspectComponentModify(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
326
|
-
{
|
|
327
|
-
var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
|
|
328
|
-
if (go == null)
|
|
329
|
-
return;
|
|
330
|
-
|
|
331
|
-
if (!TryGetArgument(invocation.Arguments, "componentRef", out var element)
|
|
332
|
-
|| element.ValueKind != JsonValueKind.Object)
|
|
333
|
-
{
|
|
334
|
-
result.Errors.Add("component_missing");
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
var reference = Deserialize<ComponentRef>(element);
|
|
339
|
-
if (reference == null || !reference.IsValid(out _))
|
|
340
|
-
{
|
|
341
|
-
result.Errors.Add("component_invalid");
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
var component = FindComponent(go, reference);
|
|
346
|
-
if (component == null)
|
|
347
|
-
{
|
|
348
|
-
result.Errors.Add("component_not_found");
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
result.Targets.Add(ComponentTarget(component));
|
|
353
|
-
result.Effects.Add("modify component '" + Bound(component.GetType().Name) + "' on GameObject '" + Bound(go.name) + "'");
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private static void InspectComponentDestroy(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
357
|
-
{
|
|
358
|
-
var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
|
|
359
|
-
if (go == null)
|
|
360
|
-
return;
|
|
361
|
-
|
|
362
|
-
if (!TryGetArgument(invocation.Arguments, "destroyComponentRefs", out var element)
|
|
363
|
-
|| element.ValueKind != JsonValueKind.Array)
|
|
364
|
-
{
|
|
365
|
-
result.Errors.Add("component_missing");
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
var references = Deserialize<ComponentRefList>(element);
|
|
370
|
-
if (references == null || references.Count == 0)
|
|
371
|
-
{
|
|
372
|
-
result.Errors.Add("component_missing");
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
var count = 0;
|
|
377
|
-
foreach (var reference in references)
|
|
378
|
-
{
|
|
379
|
-
if (reference == null || !reference.IsValid(out _))
|
|
380
|
-
{
|
|
381
|
-
result.Errors.Add("component_invalid");
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
var component = FindComponent(go, reference);
|
|
385
|
-
if (component == null)
|
|
386
|
-
{
|
|
387
|
-
result.Errors.Add("component_not_found");
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
count++;
|
|
391
|
-
if (result.Targets.Count < MaxListedTargets)
|
|
392
|
-
result.Targets.Add(ComponentTarget(component));
|
|
393
|
-
}
|
|
394
|
-
result.Effects.Add("destroy " + count + " component(s) on GameObject '" + Bound(go.name) + "'");
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
private static void InspectSceneAsset(
|
|
398
|
-
AuthoringInvocation invocation,
|
|
399
|
-
UnityPilotInspection result,
|
|
400
|
-
bool requireOpened,
|
|
401
|
-
string effect)
|
|
402
|
-
{
|
|
403
|
-
if (!TryGetArgument(invocation.Arguments, "sceneRef", out var element)
|
|
404
|
-
|| element.ValueKind != JsonValueKind.Object)
|
|
405
|
-
{
|
|
406
|
-
result.Errors.Add("target_missing");
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
var reference = Deserialize<AssetObjectRef>(element);
|
|
411
|
-
var sceneAsset = reference == null ? null : reference.FindAssetObject<SceneAsset>();
|
|
412
|
-
if (sceneAsset == null)
|
|
413
|
-
{
|
|
414
|
-
result.Errors.Add("scene_asset_not_found");
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
var assetPath = AssetDatabase.GetAssetPath(sceneAsset);
|
|
419
|
-
var guid = AssetDatabase.AssetPathToGUID(assetPath);
|
|
420
|
-
if (requireOpened)
|
|
421
|
-
{
|
|
422
|
-
var opened = UnityEngine.SceneManagement.SceneManager.GetSceneByPath(assetPath);
|
|
423
|
-
if (!opened.IsValid())
|
|
424
|
-
opened = UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneAsset.name);
|
|
425
|
-
if (!opened.IsValid() || !opened.isLoaded)
|
|
426
|
-
result.Errors.Add("scene_not_opened");
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
result.Targets.Add(new AuthoringTargetSummary
|
|
430
|
-
{
|
|
431
|
-
Kind = "SceneAsset",
|
|
432
|
-
Name = Bound(sceneAsset.name),
|
|
433
|
-
RelativePath = SafeProjectRelative(assetPath),
|
|
434
|
-
Fingerprint = Hash("scene-asset|" + assetPath + "|" + guid),
|
|
435
|
-
});
|
|
436
|
-
result.Effects.Add(effect + " '" + Bound(sceneAsset.name) + "'");
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
private static void InspectSceneCreate(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
440
|
-
{
|
|
441
|
-
if (!invocation.PathBindings.TryGetValue("path", out var binding))
|
|
442
|
-
{
|
|
443
|
-
result.Errors.Add("path_missing");
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
var relative = binding.RelativePath;
|
|
448
|
-
if (!relative.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
|
|
449
|
-
result.Errors.Add("scene_extension");
|
|
450
|
-
// Creating over an existing scene file is an overwrite, which
|
|
451
|
-
// the pilot never performs implicitly.
|
|
452
|
-
if (binding.Resolution.Exists)
|
|
453
|
-
result.Errors.Add("scene_exists");
|
|
454
|
-
|
|
455
|
-
result.Targets.Add(new AuthoringTargetSummary
|
|
456
|
-
{
|
|
457
|
-
Kind = "SceneFile",
|
|
458
|
-
Name = Bound(System.IO.Path.GetFileName(relative)),
|
|
459
|
-
RelativePath = relative,
|
|
460
|
-
Fingerprint = Hash("scene-file|" + binding.CanonicalPath + "|exists=" + binding.Resolution.Exists),
|
|
461
|
-
});
|
|
462
|
-
result.Effects.Add("create scene file '" + relative + "'");
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
private static void InspectSceneSave(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
466
|
-
{
|
|
467
|
-
TryGetString(invocation.Arguments, "openedSceneName", out var openedSceneName);
|
|
468
|
-
var scene = string.IsNullOrEmpty(openedSceneName)
|
|
469
|
-
? EditorSceneManager.GetActiveScene()
|
|
470
|
-
: SceneUtils.GetAllOpenedScenes().FirstOrDefault(candidate => candidate.name == openedSceneName);
|
|
471
|
-
if (!scene.IsValid())
|
|
472
|
-
{
|
|
473
|
-
result.Errors.Add("scene_not_found");
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
string? destination = null;
|
|
478
|
-
if (invocation.PathBindings.TryGetValue("path", out var binding))
|
|
479
|
-
{
|
|
480
|
-
destination = binding.RelativePath;
|
|
481
|
-
if (!destination.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
|
|
482
|
-
result.Errors.Add("scene_extension");
|
|
483
|
-
}
|
|
484
|
-
else if (!string.IsNullOrEmpty(scene.path))
|
|
485
|
-
{
|
|
486
|
-
destination = SafeProjectRelative(scene.path);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (string.IsNullOrEmpty(destination))
|
|
490
|
-
result.Errors.Add("scene_path_missing");
|
|
491
|
-
|
|
492
|
-
result.Targets.Add(new AuthoringTargetSummary
|
|
493
|
-
{
|
|
494
|
-
Kind = "Scene",
|
|
495
|
-
Name = Bound(scene.name),
|
|
496
|
-
RelativePath = destination,
|
|
497
|
-
Fingerprint = Hash("scene|" + scene.name + "|" + scene.path + "|" + scene.rootCount + "|" + destination),
|
|
498
|
-
});
|
|
499
|
-
result.Effects.Add("save scene '" + Bound(scene.name) + "' to '" + (destination ?? "?") + "'");
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
private static void InspectSceneUnload(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
503
|
-
{
|
|
504
|
-
TryGetString(invocation.Arguments, "name", out var name);
|
|
505
|
-
if (string.IsNullOrEmpty(name))
|
|
506
|
-
{
|
|
507
|
-
result.Errors.Add("target_missing");
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
var scene = SceneUtils.GetAllOpenedScenes().FirstOrDefault(candidate => candidate.name == name);
|
|
512
|
-
if (!scene.IsValid())
|
|
513
|
-
{
|
|
514
|
-
result.Errors.Add("scene_not_found");
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
result.Targets.Add(new AuthoringTargetSummary
|
|
519
|
-
{
|
|
520
|
-
Kind = "Scene",
|
|
521
|
-
Name = Bound(scene.name),
|
|
522
|
-
RelativePath = SafeProjectRelative(scene.path),
|
|
523
|
-
Fingerprint = Hash("scene|" + scene.name + "|" + scene.path + "|" + scene.rootCount),
|
|
524
|
-
});
|
|
525
|
-
result.Effects.Add("unload scene '" + Bound(scene.name) + "'");
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/// <summary>
|
|
529
|
-
/// Fallback for pilot-attributed tools without a dedicated inspector:
|
|
530
|
-
/// the binding still covers every reference-like argument.
|
|
531
|
-
/// </summary>
|
|
532
|
-
private static void InspectArgumentTargets(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
533
|
-
{
|
|
534
|
-
foreach (var argument in invocation.Arguments)
|
|
535
|
-
{
|
|
536
|
-
if (!LooksLikeTarget(argument.Key) && !string.Equals(argument.Key, "path", StringComparison.OrdinalIgnoreCase))
|
|
537
|
-
continue;
|
|
538
|
-
result.Targets.Add(new AuthoringTargetSummary
|
|
539
|
-
{
|
|
540
|
-
Kind = argument.Key,
|
|
541
|
-
Name = DisplayValue(argument.Value),
|
|
542
|
-
Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(
|
|
543
|
-
new Dictionary<string, JsonElement> { [argument.Key] = argument.Value.Clone() }),
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
var kind = invocation.Descriptor?.MutationKind ?? AuthoringMutationKind.Unknown;
|
|
547
|
-
result.Effects.Add(kind == AuthoringMutationKind.Unknown ? "authoring change" : kind.ToString().ToLowerInvariant());
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
private static GameObject? ResolveRequiredGameObject(
|
|
551
|
-
AuthoringInvocation invocation,
|
|
552
|
-
UnityPilotInspection result,
|
|
553
|
-
string argumentName,
|
|
554
|
-
string kind)
|
|
555
|
-
{
|
|
556
|
-
if (!TryGetArgument(invocation.Arguments, argumentName, out var element)
|
|
557
|
-
|| element.ValueKind != JsonValueKind.Object)
|
|
558
|
-
{
|
|
559
|
-
result.Errors.Add("target_missing");
|
|
560
|
-
return null;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
var reference = Deserialize<GameObjectRef>(element);
|
|
564
|
-
if (reference == null || !reference.IsValid(out _))
|
|
565
|
-
{
|
|
566
|
-
result.Errors.Add("target_invalid");
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
var go = GameObjectUtils.FindBy(reference, out _);
|
|
571
|
-
if (go == null)
|
|
572
|
-
{
|
|
573
|
-
result.Errors.Add("target_not_found");
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
result.Targets.Add(GameObjectTarget(go, kind));
|
|
578
|
-
return go;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private static UnityEngine.Component? FindComponent(GameObject go, ComponentRef reference)
|
|
582
|
-
{
|
|
583
|
-
var components = go.GetComponents<UnityEngine.Component>();
|
|
584
|
-
for (var index = 0; index < components.Length; index++)
|
|
585
|
-
{
|
|
586
|
-
var component = components[index];
|
|
587
|
-
if (component != null && reference.Matches(component, index))
|
|
588
|
-
return component;
|
|
589
|
-
}
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
private static AuthoringTargetSummary GameObjectTarget(GameObject go, string kind)
|
|
594
|
-
{
|
|
595
|
-
var components = go.GetComponents<UnityEngine.Component>();
|
|
596
|
-
var componentTypes = string.Join(",", components.Select(component => component == null ? "missing" : component.GetType().FullName));
|
|
597
|
-
var path = go.GetPath();
|
|
598
|
-
var scene = go.scene.IsValid() ? go.scene.path + "|" + go.scene.name : "no-scene";
|
|
599
|
-
return new AuthoringTargetSummary
|
|
600
|
-
{
|
|
601
|
-
Kind = kind,
|
|
602
|
-
Name = Bound(go.name),
|
|
603
|
-
RelativePath = SafeHierarchyPath(path),
|
|
604
|
-
Fingerprint = Hash(
|
|
605
|
-
"gameobject|" + StableId(go)
|
|
606
|
-
+ "|" + path
|
|
607
|
-
+ "|" + scene
|
|
608
|
-
+ "|" + go.transform.childCount
|
|
609
|
-
+ "|" + componentTypes
|
|
610
|
-
+ "|" + go.activeSelf),
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
private static AuthoringTargetSummary ComponentTarget(UnityEngine.Component component)
|
|
615
|
-
{
|
|
616
|
-
var go = component.gameObject;
|
|
617
|
-
var index = Array.IndexOf(go.GetComponents<UnityEngine.Component>(), component);
|
|
618
|
-
return new AuthoringTargetSummary
|
|
619
|
-
{
|
|
620
|
-
Kind = "Component",
|
|
621
|
-
Name = Bound(component.GetType().FullName),
|
|
622
|
-
RelativePath = SafeHierarchyPath(go.GetPath()),
|
|
623
|
-
Fingerprint = Hash(
|
|
624
|
-
"component|" + StableId(component)
|
|
625
|
-
+ "|" + component.GetType().AssemblyQualifiedName
|
|
626
|
-
+ "|" + index
|
|
627
|
-
+ "|" + StableId(go)),
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/// <summary>Identity of the active scene or open prefab stage.</summary>
|
|
632
|
-
private static string ContainerFingerprint()
|
|
633
|
-
{
|
|
634
|
-
var stage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage();
|
|
635
|
-
if (stage != null)
|
|
636
|
-
return "prefab-stage|" + stage.assetPath;
|
|
637
|
-
var scene = EditorSceneManager.GetActiveScene();
|
|
638
|
-
return "scene|" + scene.path + "|" + scene.name;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
private static string SafeSceneDisplay(UnityEngine.SceneManagement.Scene scene)
|
|
642
|
-
=> scene.IsValid() ? (SafeProjectRelative(scene.path) ?? Bound(scene.name) ?? "untitled") : "none";
|
|
643
|
-
|
|
644
|
-
private static string StableId(UnityEngine.Object value)
|
|
645
|
-
{
|
|
646
|
-
#if UNITY_6000_5_OR_NEWER
|
|
647
|
-
return value.GetEntityId().ToString();
|
|
648
|
-
#else
|
|
649
|
-
return value.GetInstanceID().ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
650
|
-
#endif
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
private static string Hash(string material)
|
|
654
|
-
{
|
|
655
|
-
using var sha = System.Security.Cryptography.SHA256.Create();
|
|
656
|
-
var bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(material));
|
|
657
|
-
var builder = new System.Text.StringBuilder(bytes.Length * 2);
|
|
658
|
-
foreach (var value in bytes)
|
|
659
|
-
builder.Append(value.ToString("x2", System.Globalization.CultureInfo.InvariantCulture));
|
|
660
|
-
return "sha256-" + builder;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
private static T? Deserialize<T>(JsonElement element) where T : class
|
|
664
|
-
{
|
|
665
|
-
try
|
|
666
|
-
{
|
|
667
|
-
var reflector = UnityCopilotPluginEditor.HasInstance
|
|
668
|
-
? UnityCopilotPluginEditor.Instance.Reflector
|
|
669
|
-
: null;
|
|
670
|
-
if (reflector != null)
|
|
671
|
-
return reflector.JsonSerializer.Deserialize<T>(reflector, element);
|
|
672
|
-
return System.Text.Json.JsonSerializer.Deserialize<T>(element.GetRawText());
|
|
673
|
-
}
|
|
674
|
-
catch
|
|
675
|
-
{
|
|
676
|
-
return null;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
private static bool TryGetArgument(
|
|
681
|
-
IReadOnlyDictionary<string, JsonElement> arguments,
|
|
682
|
-
string name,
|
|
683
|
-
out JsonElement value)
|
|
684
|
-
{
|
|
685
|
-
if (arguments.TryGetValue(name, out value))
|
|
686
|
-
return true;
|
|
687
|
-
foreach (var pair in arguments)
|
|
688
|
-
{
|
|
689
|
-
if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
|
|
690
|
-
{
|
|
691
|
-
value = pair.Value;
|
|
692
|
-
return true;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
value = default;
|
|
696
|
-
return false;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
private static bool TryGetString(
|
|
700
|
-
IReadOnlyDictionary<string, JsonElement> arguments,
|
|
701
|
-
string name,
|
|
702
|
-
out string? value)
|
|
703
|
-
{
|
|
704
|
-
value = null;
|
|
705
|
-
if (TryGetArgument(arguments, name, out var element) && element.ValueKind == JsonValueKind.String)
|
|
706
|
-
{
|
|
707
|
-
value = element.GetString();
|
|
708
|
-
return true;
|
|
709
|
-
}
|
|
710
|
-
return false;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
private static bool LooksLikeTarget(string name)
|
|
714
|
-
=> name.EndsWith("Ref", StringComparison.Ordinal)
|
|
715
|
-
|| name.EndsWith("Refs", StringComparison.Ordinal)
|
|
716
|
-
|| name.IndexOf("component", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
717
|
-
|
|
718
|
-
private static string? DisplayValue(JsonElement value)
|
|
719
|
-
{
|
|
720
|
-
if (value.ValueKind == JsonValueKind.String)
|
|
721
|
-
return Bound(value.GetString());
|
|
722
|
-
if (value.ValueKind == JsonValueKind.Object)
|
|
723
|
-
{
|
|
724
|
-
foreach (var name in new[] { "path", "name", "assetPath", "assetGuid", "instanceID", "entityId" })
|
|
725
|
-
{
|
|
726
|
-
foreach (var property in value.EnumerateObject())
|
|
727
|
-
{
|
|
728
|
-
if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
729
|
-
return DisplayValue(property.Value);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
return null;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
private static string? SafeHierarchyPath(string? path)
|
|
737
|
-
{
|
|
738
|
-
if (string.IsNullOrEmpty(path)) return null;
|
|
739
|
-
var trimmed = path!.Replace('\\', '/').TrimStart('/');
|
|
740
|
-
return trimmed.Length == 0 ? null : Bound(trimmed);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
public static string? SafeProjectRelative(string? path)
|
|
744
|
-
{
|
|
745
|
-
if (string.IsNullOrWhiteSpace(path)) return null;
|
|
746
|
-
var dataPath = Application.dataPath.Replace('\\', '/');
|
|
747
|
-
var normalized = path!.Replace('\\', '/');
|
|
748
|
-
if (normalized.StartsWith(dataPath + "/", StringComparison.OrdinalIgnoreCase))
|
|
749
|
-
return "Assets/" + normalized.Substring(dataPath.Length + 1);
|
|
750
|
-
if (normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
|
751
|
-
return normalized;
|
|
752
|
-
return null;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
private static string? Bound(string? value)
|
|
756
|
-
=> value == null ? null : value.Length <= 160 ? value : value.Substring(0, 160);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/// <summary>
|
|
760
|
-
/// Batch-level validator/planner. It inspects every child registration
|
|
761
|
-
/// through the existing tool manager, but never invokes a child runner.
|
|
762
|
-
/// The parent batch therefore cannot obtain a plan for an opaque or
|
|
763
|
-
/// unsupported child and then execute it as if it were safe.
|
|
764
|
-
/// </summary>
|
|
765
|
-
public sealed class UnityBatchAuthoringValidator : IAuthoringValidator
|
|
766
|
-
{
|
|
767
|
-
public AuthoringValidationResult Validate(AuthoringInvocation invocation)
|
|
768
|
-
{
|
|
769
|
-
if (invocation == null)
|
|
770
|
-
throw new ArgumentNullException(nameof(invocation));
|
|
771
|
-
|
|
772
|
-
var errors = UnityBatchAuthoringPlanning.Validate(invocation);
|
|
773
|
-
return new AuthoringValidationResult
|
|
774
|
-
{
|
|
775
|
-
Valid = errors.Count == 0,
|
|
776
|
-
FailureCode = errors.Count == 0 ? null : errors[0],
|
|
777
|
-
Details = errors.Count == 0
|
|
778
|
-
? null
|
|
779
|
-
: new System.Text.Json.Nodes.JsonObject { ["reason"] = errors[0] },
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
public sealed class UnityBatchAuthoringPlanner : IAuthoringPlanner
|
|
785
|
-
{
|
|
786
|
-
public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
|
|
787
|
-
{
|
|
788
|
-
if (invocation == null)
|
|
789
|
-
throw new ArgumentNullException(nameof(invocation));
|
|
790
|
-
return UnityBatchAuthoringPlanning.Plan(invocation);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
internal static class UnityBatchAuthoringPlanning
|
|
795
|
-
{
|
|
796
|
-
public static List<string> Validate(AuthoringInvocation invocation)
|
|
797
|
-
{
|
|
798
|
-
var errors = new List<string>();
|
|
799
|
-
if (!TryReadCommands(invocation, out var commands, out var error))
|
|
800
|
-
{
|
|
801
|
-
errors.Add(error ?? "validation_failed");
|
|
802
|
-
return errors;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
if (commands.Count > Tool_Batch.HardMaxCommands)
|
|
806
|
-
{
|
|
807
|
-
errors.Add("validation_failed");
|
|
808
|
-
return errors;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
for (var index = 0; index < commands.Count; index++)
|
|
812
|
-
{
|
|
813
|
-
var command = commands[index];
|
|
814
|
-
var runner = FindRunner(command.Tool);
|
|
815
|
-
if (runner == null)
|
|
816
|
-
{
|
|
817
|
-
errors.Add("safety_unsupported");
|
|
818
|
-
continue;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
if (string.Equals(command.Tool, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal))
|
|
822
|
-
{
|
|
823
|
-
errors.Add("safety_unsupported");
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
_ = AuthoringSafetyPolicy.Classify(runner);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
return errors;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
public static AuthoringPlanSummary Plan(AuthoringInvocation invocation)
|
|
834
|
-
{
|
|
835
|
-
if (!TryReadCommands(invocation, out var commands, out var error))
|
|
836
|
-
throw new ToolCallControlException(
|
|
837
|
-
ToolCallErrorCodes.InvalidControl,
|
|
838
|
-
error ?? "Batch commands are invalid.");
|
|
839
|
-
|
|
840
|
-
if (commands.Count > Tool_Batch.HardMaxCommands)
|
|
841
|
-
throw new ToolCallControlException(
|
|
842
|
-
ToolCallErrorCodes.InvalidControl,
|
|
843
|
-
"The batch contains too many commands.",
|
|
844
|
-
details: new System.Text.Json.Nodes.JsonObject
|
|
845
|
-
{
|
|
846
|
-
["reason"] = "max_commands",
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
var targets = new List<AuthoringTargetSummary>();
|
|
850
|
-
var effects = new List<string>();
|
|
851
|
-
var childRecords = new List<AuthoringChildPlanSummary>();
|
|
852
|
-
var aggregateUndo = AuthoringUndoLevel.Full;
|
|
853
|
-
var supportsSharedTransaction = true;
|
|
854
|
-
var hasMutatingChild = false;
|
|
855
|
-
var policy = AuthoringSafetyPolicyContext.Current;
|
|
856
|
-
if (policy == null)
|
|
857
|
-
{
|
|
858
|
-
throw new ToolCallControlException(
|
|
859
|
-
ToolCallErrorCodes.SafetyUnsupported,
|
|
860
|
-
"Batch planning requires the live authoring safety policy.",
|
|
861
|
-
details: new System.Text.Json.Nodes.JsonObject { ["reason"] = "policy_scope_missing" });
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
var existingChildren = invocation.PlanBeingVerified?.ChildRecords;
|
|
865
|
-
if (existingChildren != null && existingChildren.Count != commands.Count)
|
|
866
|
-
{
|
|
867
|
-
throw new ToolCallControlException(
|
|
868
|
-
ToolCallErrorCodes.ConfirmationStale,
|
|
869
|
-
"The batch child confirmation records no longer match the command list.");
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
for (var index = 0; index < commands.Count; index++)
|
|
873
|
-
{
|
|
874
|
-
var command = commands[index];
|
|
875
|
-
var runner = FindRunner(command.Tool);
|
|
876
|
-
if (runner == null
|
|
877
|
-
|| string.Equals(command.Tool, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal))
|
|
878
|
-
{
|
|
879
|
-
throw new ToolCallControlException(
|
|
880
|
-
ToolCallErrorCodes.SafetyUnsupported,
|
|
881
|
-
"A batch child does not expose a supported authoring capability.",
|
|
882
|
-
details: new System.Text.Json.Nodes.JsonObject { ["reason"] = "child_capability_missing" });
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
var childContext = ToolCallContextNormalizer.DeriveChild(
|
|
886
|
-
invocation.Context,
|
|
887
|
-
new ToolCallControl
|
|
888
|
-
{
|
|
889
|
-
CallId = "batch-plan-" + index.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
|
890
|
-
},
|
|
891
|
-
requestId: "batch-plan-request-" + index.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
|
892
|
-
cancellationToken: invocation.Context.CancellationToken);
|
|
893
|
-
var childInvocation = policy.PrepareInvocation(new AuthoringInvocation(
|
|
894
|
-
childContext,
|
|
895
|
-
runner.Name,
|
|
896
|
-
command.Parameters,
|
|
897
|
-
runner));
|
|
898
|
-
var descriptor = childInvocation.Descriptor;
|
|
899
|
-
var risk = AuthoringSafetyPolicy.Classify(
|
|
900
|
-
childInvocation.Runner.ReadOnlyHint,
|
|
901
|
-
childInvocation.Runner.DestructiveHint,
|
|
902
|
-
descriptor);
|
|
903
|
-
if (descriptor != null
|
|
904
|
-
&& ((descriptor.SupportsPlanning && descriptor.Planner == null)
|
|
905
|
-
|| (descriptor.SupportsValidation && descriptor.Validator == null
|
|
906
|
-
&& descriptor.Planner == null)))
|
|
907
|
-
{
|
|
908
|
-
throw new ToolCallControlException(
|
|
909
|
-
ToolCallErrorCodes.SafetyUnsupported,
|
|
910
|
-
"A batch child has inconsistent authoring capability data.",
|
|
911
|
-
details: new System.Text.Json.Nodes.JsonObject
|
|
912
|
-
{
|
|
913
|
-
["reason"] = "binding_unavailable",
|
|
914
|
-
["stage"] = "declared_adapter_unavailable",
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
AuthoringValidationResult? childValidation = null;
|
|
919
|
-
if (descriptor?.Validator != null)
|
|
920
|
-
{
|
|
921
|
-
childValidation = descriptor.Validator.Validate(childInvocation);
|
|
922
|
-
if (childValidation == null || !childValidation.Valid)
|
|
923
|
-
{
|
|
924
|
-
var failureCode = childValidation?.FailureCode;
|
|
925
|
-
var safeFailureCode = string.IsNullOrWhiteSpace(failureCode)
|
|
926
|
-
? "validation_failed"
|
|
927
|
-
: failureCode ?? "validation_failed";
|
|
928
|
-
throw new ToolCallControlException(
|
|
929
|
-
safeFailureCode,
|
|
930
|
-
"A batch child did not pass validation.");
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
var childPlan = descriptor?.Planner?.Plan(childInvocation);
|
|
935
|
-
if (descriptor?.Planner != null && childPlan == null)
|
|
936
|
-
{
|
|
937
|
-
throw new ToolCallControlException(
|
|
938
|
-
ToolCallErrorCodes.SafetyUnsupported,
|
|
939
|
-
"A batch child planner returned no plan.",
|
|
940
|
-
details: new System.Text.Json.Nodes.JsonObject
|
|
941
|
-
{
|
|
942
|
-
["reason"] = "binding_unavailable",
|
|
943
|
-
["stage"] = "planner_returned_null",
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
var validationTargets = childValidation?.Targets == null
|
|
948
|
-
? Array.Empty<AuthoringTargetSummary>()
|
|
949
|
-
: childValidation.Targets.Where(target => target != null).Take(8)
|
|
950
|
-
.Select(target => target.CloneSafe()).ToArray();
|
|
951
|
-
var plannedTargets = childPlan?.Targets == null
|
|
952
|
-
? Array.Empty<AuthoringTargetSummary>()
|
|
953
|
-
: childPlan.Targets.Where(target => target != null).Take(8)
|
|
954
|
-
.Select(target => target.CloneSafe()).ToArray();
|
|
955
|
-
IReadOnlyList<AuthoringTargetSummary> childTargets = plannedTargets.Length == 0
|
|
956
|
-
? validationTargets
|
|
957
|
-
: plannedTargets;
|
|
958
|
-
var plannedEffects = childPlan?.PredictedEffects == null
|
|
959
|
-
? Array.Empty<string>()
|
|
960
|
-
: childPlan.PredictedEffects.Where(effect => !string.IsNullOrWhiteSpace(effect)).Take(8)
|
|
961
|
-
.Select(effect => effect.Length <= 160 ? effect : effect.Substring(0, 160)).ToArray();
|
|
962
|
-
IReadOnlyList<string> childEffects = plannedEffects.Length > 0
|
|
963
|
-
? plannedEffects
|
|
964
|
-
: new[]
|
|
965
|
-
{
|
|
966
|
-
descriptor == null || descriptor.IsOpaque
|
|
967
|
-
? "undeclared"
|
|
968
|
-
: descriptor.MutationKind == AuthoringMutationKind.Unknown
|
|
969
|
-
? "authoring change"
|
|
970
|
-
: descriptor.MutationKind.ToString().ToLowerInvariant(),
|
|
971
|
-
};
|
|
972
|
-
|
|
973
|
-
AuthoringChildPlanSummary? existingAuthority = null;
|
|
974
|
-
if (existingChildren != null)
|
|
975
|
-
{
|
|
976
|
-
existingAuthority = existingChildren[index];
|
|
977
|
-
if (existingAuthority == null
|
|
978
|
-
|| existingAuthority.Index != index
|
|
979
|
-
|| !string.Equals(existingAuthority.ToolName, runner.Name, StringComparison.Ordinal))
|
|
980
|
-
{
|
|
981
|
-
throw new ToolCallControlException(
|
|
982
|
-
ToolCallErrorCodes.ConfirmationStale,
|
|
983
|
-
"The batch child confirmation records no longer match the command list.");
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
var childRecord = policy.CreateAggregateChildRecord(
|
|
988
|
-
index,
|
|
989
|
-
childInvocation,
|
|
990
|
-
childValidation,
|
|
991
|
-
childPlan,
|
|
992
|
-
childTargets,
|
|
993
|
-
childEffects,
|
|
994
|
-
existingAuthority);
|
|
995
|
-
childRecords.Add(childRecord);
|
|
996
|
-
|
|
997
|
-
if (risk != AuthoringRiskLevel.Read)
|
|
998
|
-
{
|
|
999
|
-
hasMutatingChild = true;
|
|
1000
|
-
aggregateUndo = (AuthoringUndoLevel)Math.Min(
|
|
1001
|
-
(int)aggregateUndo,
|
|
1002
|
-
(int)childRecord.UndoLevel);
|
|
1003
|
-
supportsSharedTransaction &= childRecord.SupportsSharedTransaction;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
targets.Add(new AuthoringTargetSummary
|
|
1007
|
-
{
|
|
1008
|
-
Kind = "batch-child",
|
|
1009
|
-
Name = runner.Name,
|
|
1010
|
-
Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(
|
|
1011
|
-
new Dictionary<string, JsonElement>
|
|
1012
|
-
{
|
|
1013
|
-
["index"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Index),
|
|
1014
|
-
["tool"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.ToolName),
|
|
1015
|
-
["argumentsHash"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.ArgumentsHash),
|
|
1016
|
-
["risk"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Risk),
|
|
1017
|
-
["undo"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Undo),
|
|
1018
|
-
["planLevel"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.PlanLevel),
|
|
1019
|
-
}),
|
|
1020
|
-
});
|
|
1021
|
-
targets.AddRange(childRecord.Targets.Select(target => target.CloneSafe()));
|
|
1022
|
-
effects.AddRange(childRecord.PredictedEffects);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (effects.Count == 0)
|
|
1026
|
-
effects.Add("authoring change");
|
|
1027
|
-
|
|
1028
|
-
return new AuthoringPlanSummary
|
|
1029
|
-
{
|
|
1030
|
-
UndoLevel = aggregateUndo,
|
|
1031
|
-
HasUndoOverride = true,
|
|
1032
|
-
SupportsSharedTransaction = hasMutatingChild && supportsSharedTransaction,
|
|
1033
|
-
Targets = targets.Take(32).Select(target => target.CloneSafe()).ToArray(),
|
|
1034
|
-
PredictedEffects = effects.Take(32).ToArray(),
|
|
1035
|
-
ChildRecords = childRecords.Take(Tool_Batch.HardMaxCommands)
|
|
1036
|
-
.Select(child => child.CloneBounded()).ToArray(),
|
|
1037
|
-
BeforeSummary = new System.Text.Json.Nodes.JsonObject
|
|
1038
|
-
{
|
|
1039
|
-
["childCount"] = commands.Count,
|
|
1040
|
-
},
|
|
1041
|
-
AfterSummary = new System.Text.Json.Nodes.JsonObject
|
|
1042
|
-
{
|
|
1043
|
-
["childCount"] = commands.Count,
|
|
1044
|
-
["predictedEffects"] = effects.Take(32).Aggregate(
|
|
1045
|
-
new System.Text.Json.Nodes.JsonArray(),
|
|
1046
|
-
(array, effect) =>
|
|
1047
|
-
{
|
|
1048
|
-
array.Add(effect);
|
|
1049
|
-
return array;
|
|
1050
|
-
}),
|
|
1051
|
-
},
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
private sealed class ParsedCommand
|
|
1056
|
-
{
|
|
1057
|
-
public string Tool { get; }
|
|
1058
|
-
public Dictionary<string, JsonElement> Parameters { get; }
|
|
1059
|
-
|
|
1060
|
-
public ParsedCommand(string tool, Dictionary<string, JsonElement> parameters)
|
|
1061
|
-
{
|
|
1062
|
-
Tool = tool;
|
|
1063
|
-
Parameters = parameters;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
private static bool TryReadCommands(
|
|
1068
|
-
AuthoringInvocation invocation,
|
|
1069
|
-
out List<ParsedCommand> commands,
|
|
1070
|
-
out string? error)
|
|
1071
|
-
{
|
|
1072
|
-
commands = new List<ParsedCommand>();
|
|
1073
|
-
error = null;
|
|
1074
|
-
if (!TryGetArgument(invocation.Arguments, "commands", out var value)
|
|
1075
|
-
|| value.ValueKind != JsonValueKind.Array)
|
|
1076
|
-
{
|
|
1077
|
-
error = "validation_failed";
|
|
1078
|
-
return false;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
foreach (var item in value.EnumerateArray())
|
|
1082
|
-
{
|
|
1083
|
-
if (item.ValueKind != JsonValueKind.Object)
|
|
1084
|
-
{
|
|
1085
|
-
error = "validation_failed";
|
|
1086
|
-
return false;
|
|
1087
|
-
}
|
|
1088
|
-
if (!TryGetProperty(item, "tool", out var toolValue)
|
|
1089
|
-
|| toolValue.ValueKind != JsonValueKind.String
|
|
1090
|
-
|| string.IsNullOrWhiteSpace(toolValue.GetString()))
|
|
1091
|
-
{
|
|
1092
|
-
error = "validation_failed";
|
|
1093
|
-
return false;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
var parameters = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
|
|
1097
|
-
if (TryGetProperty(item, "params", out var paramsValue)
|
|
1098
|
-
&& paramsValue.ValueKind != JsonValueKind.Null)
|
|
1099
|
-
{
|
|
1100
|
-
if (paramsValue.ValueKind != JsonValueKind.Object)
|
|
1101
|
-
{
|
|
1102
|
-
error = "validation_failed";
|
|
1103
|
-
return false;
|
|
1104
|
-
}
|
|
1105
|
-
foreach (var property in paramsValue.EnumerateObject())
|
|
1106
|
-
parameters[property.Name] = property.Value.Clone();
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
commands.Add(new ParsedCommand(toolValue.GetString()!, parameters));
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
if (commands.Count == 0)
|
|
1113
|
-
{
|
|
1114
|
-
error = "validation_failed";
|
|
1115
|
-
return false;
|
|
1116
|
-
}
|
|
1117
|
-
return true;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
private static IRunTool? FindRunner(string name)
|
|
1121
|
-
{
|
|
1122
|
-
if (!UnityCopilotPluginEditor.HasInstance)
|
|
1123
|
-
return null;
|
|
1124
|
-
var manager = UnityCopilotPluginEditor.Instance.Tools;
|
|
1125
|
-
if (manager == null)
|
|
1126
|
-
return null;
|
|
1127
|
-
return manager.GetAllTools()
|
|
1128
|
-
.FirstOrDefault(item => item != null && string.Equals(item.Name, name, StringComparison.Ordinal));
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
private static bool TryGetArgument(
|
|
1132
|
-
IReadOnlyDictionary<string, JsonElement> arguments,
|
|
1133
|
-
string name,
|
|
1134
|
-
out JsonElement value)
|
|
1135
|
-
{
|
|
1136
|
-
if (arguments.TryGetValue(name, out value)) return true;
|
|
1137
|
-
foreach (var pair in arguments)
|
|
1138
|
-
{
|
|
1139
|
-
if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
|
|
1140
|
-
{
|
|
1141
|
-
value = pair.Value;
|
|
1142
|
-
return true;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
value = default;
|
|
1146
|
-
return false;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
private static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
|
|
1150
|
-
{
|
|
1151
|
-
foreach (var property in element.EnumerateObject())
|
|
1152
|
-
{
|
|
1153
|
-
if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
1154
|
-
{
|
|
1155
|
-
value = property.Value;
|
|
1156
|
-
return true;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
value = default;
|
|
1160
|
-
return false;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
/// <summary>
|
|
1165
|
-
/// Read-only planner for the pilot. It performs the same live target
|
|
1166
|
-
/// inspection as the validator and adds predicted effects plus bounded
|
|
1167
|
-
/// before/after summaries. A plan for an invalid call is refused rather
|
|
1168
|
-
/// than issued, so a confirmation token can only ever describe an action
|
|
1169
|
-
/// whose targets were actually observed.
|
|
1170
|
-
/// </summary>
|
|
1171
|
-
public sealed class UnityPilotAuthoringPlanner : IAuthoringPlanner
|
|
1172
|
-
{
|
|
1173
|
-
public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
|
|
1174
|
-
{
|
|
1175
|
-
if (invocation == null)
|
|
1176
|
-
throw new ArgumentNullException(nameof(invocation));
|
|
1177
|
-
|
|
1178
|
-
var inspection = UnityPilotAuthoringInspection.Inspect(invocation);
|
|
1179
|
-
if (inspection.Errors.Count > 0)
|
|
1180
|
-
{
|
|
1181
|
-
throw new ToolCallControlException(
|
|
1182
|
-
"validation_failed",
|
|
1183
|
-
"The authoring call did not pass validation.",
|
|
1184
|
-
details: new System.Text.Json.Nodes.JsonObject
|
|
1185
|
-
{
|
|
1186
|
-
["reason"] = inspection.Errors[0],
|
|
1187
|
-
["errorCount"] = inspection.Errors.Count,
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
var kind = invocation.Descriptor?.MutationKind ?? AuthoringMutationKind.Unknown;
|
|
1192
|
-
var effects = inspection.Effects.Count == 0
|
|
1193
|
-
? new List<string> { kind == AuthoringMutationKind.Unknown ? "authoring change" : kind.ToString().ToLowerInvariant() }
|
|
1194
|
-
: inspection.Effects;
|
|
1195
|
-
return new AuthoringPlanSummary
|
|
1196
|
-
{
|
|
1197
|
-
Targets = inspection.Targets.Take(32).ToArray(),
|
|
1198
|
-
PredictedEffects = effects.Take(32).ToArray(),
|
|
1199
|
-
BeforeSummary = inspection.Before,
|
|
1200
|
-
AfterSummary = inspection.After,
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
/// <summary>Unity Undo-backed transaction factory attached to pilot runners.</summary>
|
|
1206
|
-
public sealed class UnityAuthoringTransactionFactory : IAuthoringTransactionFactory
|
|
1207
|
-
{
|
|
1208
|
-
public IAuthoringTransaction Begin(AuthoringInvocation invocation)
|
|
1209
|
-
{
|
|
1210
|
-
if (invocation == null)
|
|
1211
|
-
throw new ArgumentNullException(nameof(invocation));
|
|
1212
|
-
|
|
1213
|
-
return BeginStandalone(
|
|
1214
|
-
"
|
|
1215
|
-
invocation.EffectiveUndoLevel
|
|
1216
|
-
?? invocation.Descriptor?.UndoLevel
|
|
1217
|
-
?? AuthoringUndoLevel.Partial);
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
/// <summary>
|
|
1221
|
-
/// Opens one named Unity Undo group and returns the transaction that
|
|
1222
|
-
/// owns it. Complete collapses the group to one user-visible step;
|
|
1223
|
-
/// Abort reverts only this group (never unrelated earlier history).
|
|
1224
|
-
/// </summary>
|
|
1225
|
-
public static IAuthoringTransaction BeginStandalone(string label, AuthoringUndoLevel advertisedUndo)
|
|
1226
|
-
{
|
|
1227
|
-
if (string.IsNullOrWhiteSpace(label))
|
|
1228
|
-
throw new ArgumentException("Transaction label must be non-empty.", nameof(label));
|
|
1229
|
-
|
|
1230
|
-
// The middleware may run on the transport thread; every Undo API
|
|
1231
|
-
// call is marshalled to the main thread (inline when already there).
|
|
1232
|
-
var group = MainThread.Instance.Run(() =>
|
|
1233
|
-
{
|
|
1234
|
-
Undo.IncrementCurrentGroup();
|
|
1235
|
-
var current = Undo.GetCurrentGroup();
|
|
1236
|
-
Undo.SetCurrentGroupName(label);
|
|
1237
|
-
return current;
|
|
1238
|
-
});
|
|
1239
|
-
|
|
1240
|
-
return new UnityAuthoringTransaction(
|
|
1241
|
-
label,
|
|
1242
|
-
advertisedUndo,
|
|
1243
|
-
group,
|
|
1244
|
-
recordCreated: value => MainThread.Instance.Run(() => Undo.RegisterCreatedObjectUndo(
|
|
1245
|
-
RequireObject(value), label)),
|
|
1246
|
-
recordModified: (value, completeSnapshot) => MainThread.Instance.Run(() =>
|
|
1247
|
-
{
|
|
1248
|
-
var target = RequireObject(value);
|
|
1249
|
-
if (completeSnapshot)
|
|
1250
|
-
Undo.RegisterCompleteObjectUndo(target, label);
|
|
1251
|
-
else
|
|
1252
|
-
Undo.RecordObject(target, label);
|
|
1253
|
-
}),
|
|
1254
|
-
recordDeleted: value => MainThread.Instance.Run(() => Undo.RegisterCompleteObjectUndo(
|
|
1255
|
-
RequireObject(value), label)),
|
|
1256
|
-
complete: () => MainThread.Instance.Run(() => Undo.CollapseUndoOperations(group)),
|
|
1257
|
-
abort: () => MainThread.Instance.Run(() => Undo.RevertAllDownToGroup(group)));
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
/// <summary>
|
|
1261
|
-
/// Test/tooling helper: opens a standalone transaction, installs it as
|
|
1262
|
-
/// the ambient scope, and completes it on dispose. Production calls
|
|
1263
|
-
/// always receive their scope from <see cref="AuthoringSafetyMiddleware"/>.
|
|
1264
|
-
/// </summary>
|
|
1265
|
-
public static IDisposable BeginStandaloneScope(string label, AuthoringUndoLevel advertisedUndo = AuthoringUndoLevel.Full)
|
|
1266
|
-
{
|
|
1267
|
-
var transaction = BeginStandalone(label, advertisedUndo);
|
|
1268
|
-
var scope = AuthoringTransactionScope.Push(transaction);
|
|
1269
|
-
return new StandaloneScope(transaction, scope);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
private sealed class StandaloneScope : IDisposable
|
|
1273
|
-
{
|
|
1274
|
-
private readonly IAuthoringTransaction _transaction;
|
|
1275
|
-
private readonly IDisposable _scope;
|
|
1276
|
-
private int _disposed;
|
|
1277
|
-
|
|
1278
|
-
public StandaloneScope(IAuthoringTransaction transaction, IDisposable scope)
|
|
1279
|
-
{
|
|
1280
|
-
_transaction = transaction;
|
|
1281
|
-
_scope = scope;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
public void Dispose()
|
|
1285
|
-
{
|
|
1286
|
-
if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0)
|
|
1287
|
-
return;
|
|
1288
|
-
try
|
|
1289
|
-
{
|
|
1290
|
-
_transaction.Complete();
|
|
1291
|
-
}
|
|
1292
|
-
finally
|
|
1293
|
-
{
|
|
1294
|
-
_scope.Dispose();
|
|
1295
|
-
_transaction.Dispose();
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
private static UnityEngine.Object RequireObject(object value)
|
|
1301
|
-
=> value as UnityEngine.Object
|
|
1302
|
-
?? throw new InvalidOperationException("The authoring target is not a Unity object.");
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
/// <summary>
|
|
1306
|
-
/// Unity-aware transaction: affected-object summaries carry the object's
|
|
1307
|
-
/// type and name instead of the generic <c>ToString()</c> fallback, and
|
|
1308
|
-
/// hierarchy paths are reported as safe relative display values.
|
|
1309
|
-
/// </summary>
|
|
1310
|
-
public sealed class UnityAuthoringTransaction : AuthoringTransaction
|
|
1311
|
-
{
|
|
1312
|
-
public UnityAuthoringTransaction(
|
|
1313
|
-
string groupLabel,
|
|
1314
|
-
AuthoringUndoLevel advertisedUndo,
|
|
1315
|
-
int groupId,
|
|
1316
|
-
Action<object>? recordCreated,
|
|
1317
|
-
Action<object, bool>? recordModified,
|
|
1318
|
-
Action<object>? recordDeleted,
|
|
1319
|
-
Action? complete,
|
|
1320
|
-
Action? abort)
|
|
1321
|
-
: base(groupLabel, advertisedUndo, groupId, recordCreated, recordModified, recordDeleted, complete, abort)
|
|
1322
|
-
{
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
private readonly HashSet<string> _reported = new HashSet<string>(StringComparer.Ordinal);
|
|
1326
|
-
|
|
1327
|
-
protected override void AddAffected(object value)
|
|
1328
|
-
{
|
|
1329
|
-
if (value is UnityEngine.Object unityObject && unityObject != null)
|
|
1330
|
-
{
|
|
1331
|
-
// One object may be recorded more than once inside a call
|
|
1332
|
-
// (for example RegisterCreatedObjectUndo followed by a
|
|
1333
|
-
// complete snapshot); report it once.
|
|
1334
|
-
#if UNITY_6000_5_OR_NEWER
|
|
1335
|
-
var objectId = unityObject.GetEntityId().ToString();
|
|
1336
|
-
#else
|
|
1337
|
-
var objectId = unityObject.GetInstanceID().ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
1338
|
-
#endif
|
|
1339
|
-
var path = value is GameObject go
|
|
1340
|
-
? go.GetPath()
|
|
1341
|
-
: value is UnityEngine.Component component && component != null
|
|
1342
|
-
? component.gameObject.GetPath()
|
|
1343
|
-
: null;
|
|
1344
|
-
var summary = new AuthoringAffectedObjectSummary
|
|
1345
|
-
{
|
|
1346
|
-
Kind = value.GetType().Name,
|
|
1347
|
-
Name = unityObject.name,
|
|
1348
|
-
RelativePath = string.IsNullOrEmpty(path) ? null : path!.Replace('\\', '/').TrimStart('/'),
|
|
1349
|
-
};
|
|
1350
|
-
if (!_reported.Add(objectId))
|
|
1351
|
-
{
|
|
1352
|
-
AddAffectedEvent(summary);
|
|
1353
|
-
return;
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
AddAffectedSummary(summary);
|
|
1357
|
-
return;
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
base.AddAffected(value);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
/// <summary>
|
|
1365
|
-
/// Small helper used by pilot tools. Every mutation records through the
|
|
1366
|
-
/// ambient transaction before changing Unity state.
|
|
1367
|
-
/// </summary>
|
|
1368
|
-
public static class UnityAuthoringUndo
|
|
1369
|
-
{
|
|
1370
|
-
/// <summary>
|
|
1371
|
-
/// Fails closed before the first mutation of a pilot tool body when
|
|
1372
|
-
/// the call did not arrive through the policy pipeline. An explicit
|
|
1373
|
-
/// ambient transaction (opened by the middleware, or deliberately by
|
|
1374
|
-
/// in-process tooling/tests) is accepted; a bare reflected runner
|
|
1375
|
-
/// call is not, so no object is created and then orphaned by a later
|
|
1376
|
-
/// missing-transaction failure.
|
|
1377
|
-
/// </summary>
|
|
1378
|
-
public static void RequireAuthoringScope()
|
|
1379
|
-
{
|
|
1380
|
-
if (AuthoringTransactionScope.Current != null)
|
|
1381
|
-
return;
|
|
1382
|
-
AuthoringPolicyApproval.Require();
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
public static void RecordCreated(UnityEngine.Object value)
|
|
1386
|
-
{
|
|
1387
|
-
var transaction = AuthoringTransactionScope.Current;
|
|
1388
|
-
if (transaction == null)
|
|
1389
|
-
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1390
|
-
transaction.RecordCreated(value);
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
public static void RecordModified(UnityEngine.Object value, bool completeSnapshot = false)
|
|
1394
|
-
{
|
|
1395
|
-
var transaction = AuthoringTransactionScope.Current;
|
|
1396
|
-
if (transaction == null)
|
|
1397
|
-
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1398
|
-
transaction.RecordModified(value, completeSnapshot);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
public static void Destroy(UnityEngine.Object value)
|
|
1402
|
-
{
|
|
1403
|
-
var transaction = AuthoringTransactionScope.Current;
|
|
1404
|
-
if (transaction == null)
|
|
1405
|
-
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1406
|
-
transaction.RecordDeleted(value);
|
|
1407
|
-
Undo.DestroyObjectImmediate(value);
|
|
1408
|
-
transaction.MarkMutated();
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
/// <summary>
|
|
1412
|
-
/// Undo-aware reparenting. <c>Undo.RecordObject</c> on a Transform
|
|
1413
|
-
/// does not capture hierarchy changes; Unity's
|
|
1414
|
-
/// <c>Undo.SetTransformParent</c> records the parent change inside
|
|
1415
|
-
/// the current group.
|
|
1416
|
-
/// </summary>
|
|
1417
|
-
public static void SetParent(Transform target, Transform? parent, bool worldPositionStays)
|
|
1418
|
-
{
|
|
1419
|
-
var transaction = AuthoringTransactionScope.Current;
|
|
1420
|
-
if (transaction == null)
|
|
1421
|
-
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1422
|
-
if (target == null)
|
|
1423
|
-
throw new ArgumentNullException(nameof(target));
|
|
1424
|
-
transaction.RecordHostRegistered(target);
|
|
1425
|
-
Undo.SetTransformParent(target, parent, worldPositionStays, "
|
|
1426
|
-
transaction.MarkMutated();
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
/// <summary>
|
|
1430
|
-
/// Undo-aware component creation. <c>Undo.AddComponent</c> creates
|
|
1431
|
-
/// and registers the component in one step; a second
|
|
1432
|
-
/// <c>RegisterCreatedObjectUndo</c> would duplicate the record.
|
|
1433
|
-
/// </summary>
|
|
1434
|
-
public static UnityEngine.Component? AddComponent(GameObject target, Type componentType)
|
|
1435
|
-
{
|
|
1436
|
-
var transaction = AuthoringTransactionScope.Current;
|
|
1437
|
-
if (transaction == null)
|
|
1438
|
-
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1439
|
-
if (target == null)
|
|
1440
|
-
throw new ArgumentNullException(nameof(target));
|
|
1441
|
-
if (componentType == null)
|
|
1442
|
-
throw new ArgumentNullException(nameof(componentType));
|
|
1443
|
-
|
|
1444
|
-
var component = Undo.AddComponent(target, componentType);
|
|
1445
|
-
if (component == null)
|
|
1446
|
-
return null;
|
|
1447
|
-
transaction.RecordHostRegistered(component);
|
|
1448
|
-
transaction.MarkMutated();
|
|
1449
|
-
return component;
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
/// <summary>
|
|
1453
|
-
/// Tracks an object that a Unity API already registered with Undo
|
|
1454
|
-
/// inside the current group (for example pasteboard duplication).
|
|
1455
|
-
/// </summary>
|
|
1456
|
-
public static void RecordHostRegistered(UnityEngine.Object value)
|
|
1457
|
-
{
|
|
1458
|
-
var transaction = AuthoringTransactionScope.Current;
|
|
1459
|
-
if (transaction == null)
|
|
1460
|
-
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1461
|
-
transaction.RecordHostRegistered(value);
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
public static void MarkMutated()
|
|
1465
|
-
=> AuthoringTransactionScope.Current?.MarkMutated();
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
3
|
+
│ Author: AtelierAI │
|
|
4
|
+
│ Copyright (c) 2025 AtelierAI │
|
|
5
|
+
│ Licensed under the MIT License. │
|
|
6
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
7
|
+
*/
|
|
8
|
+
#nullable enable
|
|
9
|
+
|
|
10
|
+
using System;
|
|
11
|
+
using System.Collections.Generic;
|
|
12
|
+
using System.Linq;
|
|
13
|
+
using System.Text.Json;
|
|
14
|
+
using AIGD;
|
|
15
|
+
using com.AtelierAI.Uco.Framework;
|
|
16
|
+
using com.AtelierAI.Uco.Framework.Common.Model;
|
|
17
|
+
using com.IvanMurzak.ReflectorNet.Utils;
|
|
18
|
+
using com.AtelierAI.Unity.Copilot.Editor.API;
|
|
19
|
+
using com.AtelierAI.Unity.Copilot.Runtime.Extensions;
|
|
20
|
+
using com.AtelierAI.Unity.Copilot.Runtime.Utils;
|
|
21
|
+
using UnityEditor;
|
|
22
|
+
using UnityEditor.SceneManagement;
|
|
23
|
+
using UnityEngine;
|
|
24
|
+
|
|
25
|
+
namespace com.AtelierAI.Unity.Copilot.Editor.Utils
|
|
26
|
+
{
|
|
27
|
+
/// <summary>
|
|
28
|
+
/// Read-only validator shared by the Scene/GameObject/Component pilot.
|
|
29
|
+
/// It resolves the real Unity targets (objects, components, scenes,
|
|
30
|
+
/// scene assets, and canonical paths) on the main thread without
|
|
31
|
+
/// creating, modifying, destroying, dirtying, or saving anything. The
|
|
32
|
+
/// reflected runner is deliberately never called from this adapter.
|
|
33
|
+
/// </summary>
|
|
34
|
+
public sealed class UnityPilotAuthoringValidator : IAuthoringValidator
|
|
35
|
+
{
|
|
36
|
+
public AuthoringValidationResult Validate(AuthoringInvocation invocation)
|
|
37
|
+
{
|
|
38
|
+
if (invocation == null)
|
|
39
|
+
throw new ArgumentNullException(nameof(invocation));
|
|
40
|
+
|
|
41
|
+
var inspection = UnityPilotAuthoringInspection.Inspect(invocation);
|
|
42
|
+
return new AuthoringValidationResult
|
|
43
|
+
{
|
|
44
|
+
Valid = inspection.Errors.Count == 0,
|
|
45
|
+
FailureCode = inspection.Errors.Count == 0 ? null : "validation_failed",
|
|
46
|
+
Targets = inspection.Targets.Take(32).ToArray(),
|
|
47
|
+
Details = inspection.Errors.Count == 0
|
|
48
|
+
? null
|
|
49
|
+
: new System.Text.Json.Nodes.JsonObject
|
|
50
|
+
{
|
|
51
|
+
["reason"] = inspection.Errors[0],
|
|
52
|
+
["errorCount"] = inspection.Errors.Count,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// <summary>
|
|
59
|
+
/// Result of one read-only pilot inspection. Targets carry a state
|
|
60
|
+
/// fingerprint derived from the live Unity object (identity, hierarchy
|
|
61
|
+
/// path, owning scene/stage, component set) so a later change to the
|
|
62
|
+
/// inspected target makes an issued confirmation plan stale.
|
|
63
|
+
/// </summary>
|
|
64
|
+
internal sealed class UnityPilotInspection
|
|
65
|
+
{
|
|
66
|
+
public List<AuthoringTargetSummary> Targets { get; } = new List<AuthoringTargetSummary>();
|
|
67
|
+
public List<string> Errors { get; } = new List<string>();
|
|
68
|
+
public List<string> Effects { get; } = new List<string>();
|
|
69
|
+
public System.Text.Json.Nodes.JsonObject Before { get; } = new System.Text.Json.Nodes.JsonObject();
|
|
70
|
+
public System.Text.Json.Nodes.JsonObject After { get; } = new System.Text.Json.Nodes.JsonObject();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// <summary>
|
|
74
|
+
/// Main-thread, read-only inspection of the pilot tools' targets. Every
|
|
75
|
+
/// lookup uses the same resolution helpers the runners use
|
|
76
|
+
/// (<c>GameObjectUtils.FindBy</c>, <c>FindAssetObject</c>,
|
|
77
|
+
/// <c>ComponentRef.Matches</c>), but no Unity API with a side effect is
|
|
78
|
+
/// reachable from this type.
|
|
79
|
+
/// </summary>
|
|
80
|
+
internal static class UnityPilotAuthoringInspection
|
|
81
|
+
{
|
|
82
|
+
private const int MaxListedTargets = 32;
|
|
83
|
+
|
|
84
|
+
public static UnityPilotInspection Inspect(AuthoringInvocation invocation)
|
|
85
|
+
=> MainThread.Instance.Run(() => InspectOnMainThread(invocation));
|
|
86
|
+
|
|
87
|
+
private static UnityPilotInspection InspectOnMainThread(AuthoringInvocation invocation)
|
|
88
|
+
{
|
|
89
|
+
var result = new UnityPilotInspection();
|
|
90
|
+
foreach (var binding in invocation.PathBindings.Values)
|
|
91
|
+
{
|
|
92
|
+
if (binding.Resolution.IsReadOnly
|
|
93
|
+
&& binding.Resolution.Intent != ProjectPathAccessIntent.Read)
|
|
94
|
+
result.Errors.Add("path_read_only");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (invocation.Name)
|
|
98
|
+
{
|
|
99
|
+
case Tool_GameObject.GameObjectCreateToolId:
|
|
100
|
+
InspectGameObjectCreate(invocation, result);
|
|
101
|
+
break;
|
|
102
|
+
case Tool_GameObject.GameObjectDestroyToolId:
|
|
103
|
+
InspectSingleGameObject(invocation, result, "gameObjectRef", "destroy");
|
|
104
|
+
break;
|
|
105
|
+
case Tool_GameObject.GameObjectModifyToolId:
|
|
106
|
+
InspectGameObjectList(invocation, result, "modify");
|
|
107
|
+
break;
|
|
108
|
+
case Tool_GameObject.GameObjectDuplicateToolId:
|
|
109
|
+
InspectGameObjectList(invocation, result, "duplicate");
|
|
110
|
+
break;
|
|
111
|
+
case Tool_GameObject.GameObjectSetParentToolId:
|
|
112
|
+
InspectGameObjectList(invocation, result, "parent");
|
|
113
|
+
InspectOptionalOrRequiredGameObject(invocation, result, "parentGameObjectRef", required: true, kind: "Parent");
|
|
114
|
+
break;
|
|
115
|
+
case Tool_GameObject.GameObjectComponentAddToolId:
|
|
116
|
+
InspectComponentAdd(invocation, result);
|
|
117
|
+
break;
|
|
118
|
+
case Tool_GameObject.GameObjectComponentModifyToolId:
|
|
119
|
+
InspectComponentModify(invocation, result);
|
|
120
|
+
break;
|
|
121
|
+
case Tool_GameObject.GameObjectComponentDestroyToolId:
|
|
122
|
+
InspectComponentDestroy(invocation, result);
|
|
123
|
+
break;
|
|
124
|
+
case Tool_Scene.SceneOpenToolId:
|
|
125
|
+
InspectSceneAsset(invocation, result, requireOpened: false, effect: "open scene");
|
|
126
|
+
break;
|
|
127
|
+
case Tool_Scene.SceneSetActiveToolId:
|
|
128
|
+
InspectSceneAsset(invocation, result, requireOpened: true, effect: "set active scene");
|
|
129
|
+
break;
|
|
130
|
+
case Tool_Scene.SceneCreateToolId:
|
|
131
|
+
InspectSceneCreate(invocation, result);
|
|
132
|
+
break;
|
|
133
|
+
case Tool_Scene.SceneSaveToolId:
|
|
134
|
+
InspectSceneSave(invocation, result);
|
|
135
|
+
break;
|
|
136
|
+
case Tool_Scene.SceneUnloadToolId:
|
|
137
|
+
InspectSceneUnload(invocation, result);
|
|
138
|
+
break;
|
|
139
|
+
default:
|
|
140
|
+
InspectArgumentTargets(invocation, result);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
result.Before["targetCount"] = result.Targets.Count;
|
|
145
|
+
result.Before["activeScene"] = SafeSceneDisplay(EditorSceneManager.GetActiveScene());
|
|
146
|
+
result.After["targetCount"] = result.Targets.Count;
|
|
147
|
+
result.After["predictedEffects"] = result.Effects.Take(32).Aggregate(
|
|
148
|
+
new System.Text.Json.Nodes.JsonArray(),
|
|
149
|
+
(array, effect) =>
|
|
150
|
+
{
|
|
151
|
+
array.Add(effect);
|
|
152
|
+
return array;
|
|
153
|
+
});
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private static void InspectGameObjectCreate(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
158
|
+
{
|
|
159
|
+
TryGetString(invocation.Arguments, "name", out var name);
|
|
160
|
+
if (string.IsNullOrEmpty(name))
|
|
161
|
+
result.Errors.Add("name_missing");
|
|
162
|
+
|
|
163
|
+
var parentFingerprint = string.Empty;
|
|
164
|
+
if (TryGetArgument(invocation.Arguments, "parentGameObjectRef", out var parentElement)
|
|
165
|
+
&& parentElement.ValueKind == JsonValueKind.Object)
|
|
166
|
+
{
|
|
167
|
+
var parentRef = Deserialize<GameObjectRef>(parentElement);
|
|
168
|
+
if (parentRef != null && parentRef.IsValid(out _))
|
|
169
|
+
{
|
|
170
|
+
var parent = GameObjectUtils.FindBy(parentRef, out var error);
|
|
171
|
+
if (parent == null)
|
|
172
|
+
{
|
|
173
|
+
result.Errors.Add("parent_not_found");
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
{
|
|
177
|
+
var target = GameObjectTarget(parent, "Parent");
|
|
178
|
+
parentFingerprint = target.Fingerprint ?? string.Empty;
|
|
179
|
+
result.Targets.Add(target);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
var container = ContainerFingerprint();
|
|
185
|
+
result.Targets.Add(new AuthoringTargetSummary
|
|
186
|
+
{
|
|
187
|
+
Kind = "GameObject",
|
|
188
|
+
Name = Bound(name),
|
|
189
|
+
Fingerprint = Hash("create|" + name + "|" + container + "|" + parentFingerprint),
|
|
190
|
+
});
|
|
191
|
+
result.Effects.Add("create GameObject '" + Bound(name) + "'");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private static void InspectSingleGameObject(
|
|
195
|
+
AuthoringInvocation invocation,
|
|
196
|
+
UnityPilotInspection result,
|
|
197
|
+
string argumentName,
|
|
198
|
+
string effect)
|
|
199
|
+
{
|
|
200
|
+
var go = ResolveRequiredGameObject(invocation, result, argumentName, "GameObject");
|
|
201
|
+
if (go == null)
|
|
202
|
+
return;
|
|
203
|
+
result.Effects.Add(effect + " GameObject '" + Bound(go.name) + "' (" + go.transform.childCount + " children)");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private static void InspectOptionalOrRequiredGameObject(
|
|
207
|
+
AuthoringInvocation invocation,
|
|
208
|
+
UnityPilotInspection result,
|
|
209
|
+
string argumentName,
|
|
210
|
+
bool required,
|
|
211
|
+
string kind)
|
|
212
|
+
{
|
|
213
|
+
if (!TryGetArgument(invocation.Arguments, argumentName, out var element)
|
|
214
|
+
|| element.ValueKind != JsonValueKind.Object)
|
|
215
|
+
{
|
|
216
|
+
if (required)
|
|
217
|
+
result.Errors.Add("target_missing");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
var reference = Deserialize<GameObjectRef>(element);
|
|
222
|
+
if (reference == null || !reference.IsValid(out _))
|
|
223
|
+
{
|
|
224
|
+
if (required)
|
|
225
|
+
result.Errors.Add("target_invalid");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
var go = GameObjectUtils.FindBy(reference, out _);
|
|
230
|
+
if (go == null)
|
|
231
|
+
{
|
|
232
|
+
result.Errors.Add("target_not_found");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
result.Targets.Add(GameObjectTarget(go, kind));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private static void InspectGameObjectList(
|
|
239
|
+
AuthoringInvocation invocation,
|
|
240
|
+
UnityPilotInspection result,
|
|
241
|
+
string effect)
|
|
242
|
+
{
|
|
243
|
+
if (!TryGetArgument(invocation.Arguments, "gameObjectRefs", out var element)
|
|
244
|
+
|| element.ValueKind != JsonValueKind.Array)
|
|
245
|
+
{
|
|
246
|
+
result.Errors.Add("target_missing");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var list = Deserialize<GameObjectRefList>(element);
|
|
251
|
+
if (list == null || list.Count == 0)
|
|
252
|
+
{
|
|
253
|
+
result.Errors.Add("target_missing");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
var resolved = 0;
|
|
258
|
+
foreach (var reference in list)
|
|
259
|
+
{
|
|
260
|
+
if (reference == null || !reference.IsValid(out _))
|
|
261
|
+
{
|
|
262
|
+
result.Errors.Add("target_invalid");
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
var go = GameObjectUtils.FindBy(reference, out _);
|
|
266
|
+
if (go == null)
|
|
267
|
+
{
|
|
268
|
+
result.Errors.Add("target_not_found");
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
resolved++;
|
|
272
|
+
if (result.Targets.Count < MaxListedTargets)
|
|
273
|
+
result.Targets.Add(GameObjectTarget(go, "GameObject"));
|
|
274
|
+
}
|
|
275
|
+
result.Effects.Add(effect + " " + resolved + " GameObject(s)");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private static void InspectComponentAdd(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
279
|
+
{
|
|
280
|
+
var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
|
|
281
|
+
if (!TryGetArgument(invocation.Arguments, "componentNames", out var namesElement)
|
|
282
|
+
|| namesElement.ValueKind != JsonValueKind.Array)
|
|
283
|
+
{
|
|
284
|
+
result.Errors.Add("component_names_missing");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
var count = 0;
|
|
289
|
+
foreach (var item in namesElement.EnumerateArray())
|
|
290
|
+
{
|
|
291
|
+
if (item.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetString()))
|
|
292
|
+
{
|
|
293
|
+
result.Errors.Add("component_name_invalid");
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var componentName = item.GetString()!;
|
|
298
|
+
var type = TypeUtils.GetType(componentName)
|
|
299
|
+
?? Tool_GameObject.AllComponentTypes.FirstOrDefault(t => t.Name == componentName);
|
|
300
|
+
if (type == null)
|
|
301
|
+
{
|
|
302
|
+
result.Errors.Add("component_type_not_found");
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (!typeof(UnityEngine.Component).IsAssignableFrom(type))
|
|
306
|
+
{
|
|
307
|
+
result.Errors.Add("component_type_not_component");
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
count++;
|
|
311
|
+
result.Targets.Add(new AuthoringTargetSummary
|
|
312
|
+
{
|
|
313
|
+
Kind = "ComponentType",
|
|
314
|
+
Name = Bound(type.FullName),
|
|
315
|
+
Fingerprint = Hash("component-type|" + type.AssemblyQualifiedName),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (count == 0 && result.Errors.Count == 0)
|
|
320
|
+
result.Errors.Add("component_names_missing");
|
|
321
|
+
if (go != null)
|
|
322
|
+
result.Effects.Add("add " + count + " component(s) to GameObject '" + Bound(go.name) + "'");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private static void InspectComponentModify(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
326
|
+
{
|
|
327
|
+
var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
|
|
328
|
+
if (go == null)
|
|
329
|
+
return;
|
|
330
|
+
|
|
331
|
+
if (!TryGetArgument(invocation.Arguments, "componentRef", out var element)
|
|
332
|
+
|| element.ValueKind != JsonValueKind.Object)
|
|
333
|
+
{
|
|
334
|
+
result.Errors.Add("component_missing");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
var reference = Deserialize<ComponentRef>(element);
|
|
339
|
+
if (reference == null || !reference.IsValid(out _))
|
|
340
|
+
{
|
|
341
|
+
result.Errors.Add("component_invalid");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
var component = FindComponent(go, reference);
|
|
346
|
+
if (component == null)
|
|
347
|
+
{
|
|
348
|
+
result.Errors.Add("component_not_found");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
result.Targets.Add(ComponentTarget(component));
|
|
353
|
+
result.Effects.Add("modify component '" + Bound(component.GetType().Name) + "' on GameObject '" + Bound(go.name) + "'");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private static void InspectComponentDestroy(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
357
|
+
{
|
|
358
|
+
var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
|
|
359
|
+
if (go == null)
|
|
360
|
+
return;
|
|
361
|
+
|
|
362
|
+
if (!TryGetArgument(invocation.Arguments, "destroyComponentRefs", out var element)
|
|
363
|
+
|| element.ValueKind != JsonValueKind.Array)
|
|
364
|
+
{
|
|
365
|
+
result.Errors.Add("component_missing");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
var references = Deserialize<ComponentRefList>(element);
|
|
370
|
+
if (references == null || references.Count == 0)
|
|
371
|
+
{
|
|
372
|
+
result.Errors.Add("component_missing");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
var count = 0;
|
|
377
|
+
foreach (var reference in references)
|
|
378
|
+
{
|
|
379
|
+
if (reference == null || !reference.IsValid(out _))
|
|
380
|
+
{
|
|
381
|
+
result.Errors.Add("component_invalid");
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
var component = FindComponent(go, reference);
|
|
385
|
+
if (component == null)
|
|
386
|
+
{
|
|
387
|
+
result.Errors.Add("component_not_found");
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
count++;
|
|
391
|
+
if (result.Targets.Count < MaxListedTargets)
|
|
392
|
+
result.Targets.Add(ComponentTarget(component));
|
|
393
|
+
}
|
|
394
|
+
result.Effects.Add("destroy " + count + " component(s) on GameObject '" + Bound(go.name) + "'");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private static void InspectSceneAsset(
|
|
398
|
+
AuthoringInvocation invocation,
|
|
399
|
+
UnityPilotInspection result,
|
|
400
|
+
bool requireOpened,
|
|
401
|
+
string effect)
|
|
402
|
+
{
|
|
403
|
+
if (!TryGetArgument(invocation.Arguments, "sceneRef", out var element)
|
|
404
|
+
|| element.ValueKind != JsonValueKind.Object)
|
|
405
|
+
{
|
|
406
|
+
result.Errors.Add("target_missing");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
var reference = Deserialize<AssetObjectRef>(element);
|
|
411
|
+
var sceneAsset = reference == null ? null : reference.FindAssetObject<SceneAsset>();
|
|
412
|
+
if (sceneAsset == null)
|
|
413
|
+
{
|
|
414
|
+
result.Errors.Add("scene_asset_not_found");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
var assetPath = AssetDatabase.GetAssetPath(sceneAsset);
|
|
419
|
+
var guid = AssetDatabase.AssetPathToGUID(assetPath);
|
|
420
|
+
if (requireOpened)
|
|
421
|
+
{
|
|
422
|
+
var opened = UnityEngine.SceneManagement.SceneManager.GetSceneByPath(assetPath);
|
|
423
|
+
if (!opened.IsValid())
|
|
424
|
+
opened = UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneAsset.name);
|
|
425
|
+
if (!opened.IsValid() || !opened.isLoaded)
|
|
426
|
+
result.Errors.Add("scene_not_opened");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
result.Targets.Add(new AuthoringTargetSummary
|
|
430
|
+
{
|
|
431
|
+
Kind = "SceneAsset",
|
|
432
|
+
Name = Bound(sceneAsset.name),
|
|
433
|
+
RelativePath = SafeProjectRelative(assetPath),
|
|
434
|
+
Fingerprint = Hash("scene-asset|" + assetPath + "|" + guid),
|
|
435
|
+
});
|
|
436
|
+
result.Effects.Add(effect + " '" + Bound(sceneAsset.name) + "'");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private static void InspectSceneCreate(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
440
|
+
{
|
|
441
|
+
if (!invocation.PathBindings.TryGetValue("path", out var binding))
|
|
442
|
+
{
|
|
443
|
+
result.Errors.Add("path_missing");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
var relative = binding.RelativePath;
|
|
448
|
+
if (!relative.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
|
|
449
|
+
result.Errors.Add("scene_extension");
|
|
450
|
+
// Creating over an existing scene file is an overwrite, which
|
|
451
|
+
// the pilot never performs implicitly.
|
|
452
|
+
if (binding.Resolution.Exists)
|
|
453
|
+
result.Errors.Add("scene_exists");
|
|
454
|
+
|
|
455
|
+
result.Targets.Add(new AuthoringTargetSummary
|
|
456
|
+
{
|
|
457
|
+
Kind = "SceneFile",
|
|
458
|
+
Name = Bound(System.IO.Path.GetFileName(relative)),
|
|
459
|
+
RelativePath = relative,
|
|
460
|
+
Fingerprint = Hash("scene-file|" + binding.CanonicalPath + "|exists=" + binding.Resolution.Exists),
|
|
461
|
+
});
|
|
462
|
+
result.Effects.Add("create scene file '" + relative + "'");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private static void InspectSceneSave(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
466
|
+
{
|
|
467
|
+
TryGetString(invocation.Arguments, "openedSceneName", out var openedSceneName);
|
|
468
|
+
var scene = string.IsNullOrEmpty(openedSceneName)
|
|
469
|
+
? EditorSceneManager.GetActiveScene()
|
|
470
|
+
: SceneUtils.GetAllOpenedScenes().FirstOrDefault(candidate => candidate.name == openedSceneName);
|
|
471
|
+
if (!scene.IsValid())
|
|
472
|
+
{
|
|
473
|
+
result.Errors.Add("scene_not_found");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
string? destination = null;
|
|
478
|
+
if (invocation.PathBindings.TryGetValue("path", out var binding))
|
|
479
|
+
{
|
|
480
|
+
destination = binding.RelativePath;
|
|
481
|
+
if (!destination.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
|
|
482
|
+
result.Errors.Add("scene_extension");
|
|
483
|
+
}
|
|
484
|
+
else if (!string.IsNullOrEmpty(scene.path))
|
|
485
|
+
{
|
|
486
|
+
destination = SafeProjectRelative(scene.path);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (string.IsNullOrEmpty(destination))
|
|
490
|
+
result.Errors.Add("scene_path_missing");
|
|
491
|
+
|
|
492
|
+
result.Targets.Add(new AuthoringTargetSummary
|
|
493
|
+
{
|
|
494
|
+
Kind = "Scene",
|
|
495
|
+
Name = Bound(scene.name),
|
|
496
|
+
RelativePath = destination,
|
|
497
|
+
Fingerprint = Hash("scene|" + scene.name + "|" + scene.path + "|" + scene.rootCount + "|" + destination),
|
|
498
|
+
});
|
|
499
|
+
result.Effects.Add("save scene '" + Bound(scene.name) + "' to '" + (destination ?? "?") + "'");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private static void InspectSceneUnload(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
503
|
+
{
|
|
504
|
+
TryGetString(invocation.Arguments, "name", out var name);
|
|
505
|
+
if (string.IsNullOrEmpty(name))
|
|
506
|
+
{
|
|
507
|
+
result.Errors.Add("target_missing");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
var scene = SceneUtils.GetAllOpenedScenes().FirstOrDefault(candidate => candidate.name == name);
|
|
512
|
+
if (!scene.IsValid())
|
|
513
|
+
{
|
|
514
|
+
result.Errors.Add("scene_not_found");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
result.Targets.Add(new AuthoringTargetSummary
|
|
519
|
+
{
|
|
520
|
+
Kind = "Scene",
|
|
521
|
+
Name = Bound(scene.name),
|
|
522
|
+
RelativePath = SafeProjectRelative(scene.path),
|
|
523
|
+
Fingerprint = Hash("scene|" + scene.name + "|" + scene.path + "|" + scene.rootCount),
|
|
524
|
+
});
|
|
525
|
+
result.Effects.Add("unload scene '" + Bound(scene.name) + "'");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/// <summary>
|
|
529
|
+
/// Fallback for pilot-attributed tools without a dedicated inspector:
|
|
530
|
+
/// the binding still covers every reference-like argument.
|
|
531
|
+
/// </summary>
|
|
532
|
+
private static void InspectArgumentTargets(AuthoringInvocation invocation, UnityPilotInspection result)
|
|
533
|
+
{
|
|
534
|
+
foreach (var argument in invocation.Arguments)
|
|
535
|
+
{
|
|
536
|
+
if (!LooksLikeTarget(argument.Key) && !string.Equals(argument.Key, "path", StringComparison.OrdinalIgnoreCase))
|
|
537
|
+
continue;
|
|
538
|
+
result.Targets.Add(new AuthoringTargetSummary
|
|
539
|
+
{
|
|
540
|
+
Kind = argument.Key,
|
|
541
|
+
Name = DisplayValue(argument.Value),
|
|
542
|
+
Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(
|
|
543
|
+
new Dictionary<string, JsonElement> { [argument.Key] = argument.Value.Clone() }),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
var kind = invocation.Descriptor?.MutationKind ?? AuthoringMutationKind.Unknown;
|
|
547
|
+
result.Effects.Add(kind == AuthoringMutationKind.Unknown ? "authoring change" : kind.ToString().ToLowerInvariant());
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private static GameObject? ResolveRequiredGameObject(
|
|
551
|
+
AuthoringInvocation invocation,
|
|
552
|
+
UnityPilotInspection result,
|
|
553
|
+
string argumentName,
|
|
554
|
+
string kind)
|
|
555
|
+
{
|
|
556
|
+
if (!TryGetArgument(invocation.Arguments, argumentName, out var element)
|
|
557
|
+
|| element.ValueKind != JsonValueKind.Object)
|
|
558
|
+
{
|
|
559
|
+
result.Errors.Add("target_missing");
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
var reference = Deserialize<GameObjectRef>(element);
|
|
564
|
+
if (reference == null || !reference.IsValid(out _))
|
|
565
|
+
{
|
|
566
|
+
result.Errors.Add("target_invalid");
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
var go = GameObjectUtils.FindBy(reference, out _);
|
|
571
|
+
if (go == null)
|
|
572
|
+
{
|
|
573
|
+
result.Errors.Add("target_not_found");
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
result.Targets.Add(GameObjectTarget(go, kind));
|
|
578
|
+
return go;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private static UnityEngine.Component? FindComponent(GameObject go, ComponentRef reference)
|
|
582
|
+
{
|
|
583
|
+
var components = go.GetComponents<UnityEngine.Component>();
|
|
584
|
+
for (var index = 0; index < components.Length; index++)
|
|
585
|
+
{
|
|
586
|
+
var component = components[index];
|
|
587
|
+
if (component != null && reference.Matches(component, index))
|
|
588
|
+
return component;
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private static AuthoringTargetSummary GameObjectTarget(GameObject go, string kind)
|
|
594
|
+
{
|
|
595
|
+
var components = go.GetComponents<UnityEngine.Component>();
|
|
596
|
+
var componentTypes = string.Join(",", components.Select(component => component == null ? "missing" : component.GetType().FullName));
|
|
597
|
+
var path = go.GetPath();
|
|
598
|
+
var scene = go.scene.IsValid() ? go.scene.path + "|" + go.scene.name : "no-scene";
|
|
599
|
+
return new AuthoringTargetSummary
|
|
600
|
+
{
|
|
601
|
+
Kind = kind,
|
|
602
|
+
Name = Bound(go.name),
|
|
603
|
+
RelativePath = SafeHierarchyPath(path),
|
|
604
|
+
Fingerprint = Hash(
|
|
605
|
+
"gameobject|" + StableId(go)
|
|
606
|
+
+ "|" + path
|
|
607
|
+
+ "|" + scene
|
|
608
|
+
+ "|" + go.transform.childCount
|
|
609
|
+
+ "|" + componentTypes
|
|
610
|
+
+ "|" + go.activeSelf),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private static AuthoringTargetSummary ComponentTarget(UnityEngine.Component component)
|
|
615
|
+
{
|
|
616
|
+
var go = component.gameObject;
|
|
617
|
+
var index = Array.IndexOf(go.GetComponents<UnityEngine.Component>(), component);
|
|
618
|
+
return new AuthoringTargetSummary
|
|
619
|
+
{
|
|
620
|
+
Kind = "Component",
|
|
621
|
+
Name = Bound(component.GetType().FullName),
|
|
622
|
+
RelativePath = SafeHierarchyPath(go.GetPath()),
|
|
623
|
+
Fingerprint = Hash(
|
|
624
|
+
"component|" + StableId(component)
|
|
625
|
+
+ "|" + component.GetType().AssemblyQualifiedName
|
|
626
|
+
+ "|" + index
|
|
627
|
+
+ "|" + StableId(go)),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/// <summary>Identity of the active scene or open prefab stage.</summary>
|
|
632
|
+
private static string ContainerFingerprint()
|
|
633
|
+
{
|
|
634
|
+
var stage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage();
|
|
635
|
+
if (stage != null)
|
|
636
|
+
return "prefab-stage|" + stage.assetPath;
|
|
637
|
+
var scene = EditorSceneManager.GetActiveScene();
|
|
638
|
+
return "scene|" + scene.path + "|" + scene.name;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private static string SafeSceneDisplay(UnityEngine.SceneManagement.Scene scene)
|
|
642
|
+
=> scene.IsValid() ? (SafeProjectRelative(scene.path) ?? Bound(scene.name) ?? "untitled") : "none";
|
|
643
|
+
|
|
644
|
+
private static string StableId(UnityEngine.Object value)
|
|
645
|
+
{
|
|
646
|
+
#if UNITY_6000_5_OR_NEWER
|
|
647
|
+
return value.GetEntityId().ToString();
|
|
648
|
+
#else
|
|
649
|
+
return value.GetInstanceID().ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
650
|
+
#endif
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private static string Hash(string material)
|
|
654
|
+
{
|
|
655
|
+
using var sha = System.Security.Cryptography.SHA256.Create();
|
|
656
|
+
var bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(material));
|
|
657
|
+
var builder = new System.Text.StringBuilder(bytes.Length * 2);
|
|
658
|
+
foreach (var value in bytes)
|
|
659
|
+
builder.Append(value.ToString("x2", System.Globalization.CultureInfo.InvariantCulture));
|
|
660
|
+
return "sha256-" + builder;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private static T? Deserialize<T>(JsonElement element) where T : class
|
|
664
|
+
{
|
|
665
|
+
try
|
|
666
|
+
{
|
|
667
|
+
var reflector = UnityCopilotPluginEditor.HasInstance
|
|
668
|
+
? UnityCopilotPluginEditor.Instance.Reflector
|
|
669
|
+
: null;
|
|
670
|
+
if (reflector != null)
|
|
671
|
+
return reflector.JsonSerializer.Deserialize<T>(reflector, element);
|
|
672
|
+
return System.Text.Json.JsonSerializer.Deserialize<T>(element.GetRawText());
|
|
673
|
+
}
|
|
674
|
+
catch
|
|
675
|
+
{
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private static bool TryGetArgument(
|
|
681
|
+
IReadOnlyDictionary<string, JsonElement> arguments,
|
|
682
|
+
string name,
|
|
683
|
+
out JsonElement value)
|
|
684
|
+
{
|
|
685
|
+
if (arguments.TryGetValue(name, out value))
|
|
686
|
+
return true;
|
|
687
|
+
foreach (var pair in arguments)
|
|
688
|
+
{
|
|
689
|
+
if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
|
|
690
|
+
{
|
|
691
|
+
value = pair.Value;
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
value = default;
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private static bool TryGetString(
|
|
700
|
+
IReadOnlyDictionary<string, JsonElement> arguments,
|
|
701
|
+
string name,
|
|
702
|
+
out string? value)
|
|
703
|
+
{
|
|
704
|
+
value = null;
|
|
705
|
+
if (TryGetArgument(arguments, name, out var element) && element.ValueKind == JsonValueKind.String)
|
|
706
|
+
{
|
|
707
|
+
value = element.GetString();
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private static bool LooksLikeTarget(string name)
|
|
714
|
+
=> name.EndsWith("Ref", StringComparison.Ordinal)
|
|
715
|
+
|| name.EndsWith("Refs", StringComparison.Ordinal)
|
|
716
|
+
|| name.IndexOf("component", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
717
|
+
|
|
718
|
+
private static string? DisplayValue(JsonElement value)
|
|
719
|
+
{
|
|
720
|
+
if (value.ValueKind == JsonValueKind.String)
|
|
721
|
+
return Bound(value.GetString());
|
|
722
|
+
if (value.ValueKind == JsonValueKind.Object)
|
|
723
|
+
{
|
|
724
|
+
foreach (var name in new[] { "path", "name", "assetPath", "assetGuid", "instanceID", "entityId" })
|
|
725
|
+
{
|
|
726
|
+
foreach (var property in value.EnumerateObject())
|
|
727
|
+
{
|
|
728
|
+
if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
729
|
+
return DisplayValue(property.Value);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
private static string? SafeHierarchyPath(string? path)
|
|
737
|
+
{
|
|
738
|
+
if (string.IsNullOrEmpty(path)) return null;
|
|
739
|
+
var trimmed = path!.Replace('\\', '/').TrimStart('/');
|
|
740
|
+
return trimmed.Length == 0 ? null : Bound(trimmed);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
public static string? SafeProjectRelative(string? path)
|
|
744
|
+
{
|
|
745
|
+
if (string.IsNullOrWhiteSpace(path)) return null;
|
|
746
|
+
var dataPath = Application.dataPath.Replace('\\', '/');
|
|
747
|
+
var normalized = path!.Replace('\\', '/');
|
|
748
|
+
if (normalized.StartsWith(dataPath + "/", StringComparison.OrdinalIgnoreCase))
|
|
749
|
+
return "Assets/" + normalized.Substring(dataPath.Length + 1);
|
|
750
|
+
if (normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
|
751
|
+
return normalized;
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private static string? Bound(string? value)
|
|
756
|
+
=> value == null ? null : value.Length <= 160 ? value : value.Substring(0, 160);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/// <summary>
|
|
760
|
+
/// Batch-level validator/planner. It inspects every child registration
|
|
761
|
+
/// through the existing tool manager, but never invokes a child runner.
|
|
762
|
+
/// The parent batch therefore cannot obtain a plan for an opaque or
|
|
763
|
+
/// unsupported child and then execute it as if it were safe.
|
|
764
|
+
/// </summary>
|
|
765
|
+
public sealed class UnityBatchAuthoringValidator : IAuthoringValidator
|
|
766
|
+
{
|
|
767
|
+
public AuthoringValidationResult Validate(AuthoringInvocation invocation)
|
|
768
|
+
{
|
|
769
|
+
if (invocation == null)
|
|
770
|
+
throw new ArgumentNullException(nameof(invocation));
|
|
771
|
+
|
|
772
|
+
var errors = UnityBatchAuthoringPlanning.Validate(invocation);
|
|
773
|
+
return new AuthoringValidationResult
|
|
774
|
+
{
|
|
775
|
+
Valid = errors.Count == 0,
|
|
776
|
+
FailureCode = errors.Count == 0 ? null : errors[0],
|
|
777
|
+
Details = errors.Count == 0
|
|
778
|
+
? null
|
|
779
|
+
: new System.Text.Json.Nodes.JsonObject { ["reason"] = errors[0] },
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
public sealed class UnityBatchAuthoringPlanner : IAuthoringPlanner
|
|
785
|
+
{
|
|
786
|
+
public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
|
|
787
|
+
{
|
|
788
|
+
if (invocation == null)
|
|
789
|
+
throw new ArgumentNullException(nameof(invocation));
|
|
790
|
+
return UnityBatchAuthoringPlanning.Plan(invocation);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
internal static class UnityBatchAuthoringPlanning
|
|
795
|
+
{
|
|
796
|
+
public static List<string> Validate(AuthoringInvocation invocation)
|
|
797
|
+
{
|
|
798
|
+
var errors = new List<string>();
|
|
799
|
+
if (!TryReadCommands(invocation, out var commands, out var error))
|
|
800
|
+
{
|
|
801
|
+
errors.Add(error ?? "validation_failed");
|
|
802
|
+
return errors;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (commands.Count > Tool_Batch.HardMaxCommands)
|
|
806
|
+
{
|
|
807
|
+
errors.Add("validation_failed");
|
|
808
|
+
return errors;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
for (var index = 0; index < commands.Count; index++)
|
|
812
|
+
{
|
|
813
|
+
var command = commands[index];
|
|
814
|
+
var runner = FindRunner(command.Tool);
|
|
815
|
+
if (runner == null)
|
|
816
|
+
{
|
|
817
|
+
errors.Add("safety_unsupported");
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (string.Equals(command.Tool, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal))
|
|
822
|
+
{
|
|
823
|
+
errors.Add("safety_unsupported");
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
_ = AuthoringSafetyPolicy.Classify(runner);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return errors;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
public static AuthoringPlanSummary Plan(AuthoringInvocation invocation)
|
|
834
|
+
{
|
|
835
|
+
if (!TryReadCommands(invocation, out var commands, out var error))
|
|
836
|
+
throw new ToolCallControlException(
|
|
837
|
+
ToolCallErrorCodes.InvalidControl,
|
|
838
|
+
error ?? "Batch commands are invalid.");
|
|
839
|
+
|
|
840
|
+
if (commands.Count > Tool_Batch.HardMaxCommands)
|
|
841
|
+
throw new ToolCallControlException(
|
|
842
|
+
ToolCallErrorCodes.InvalidControl,
|
|
843
|
+
"The batch contains too many commands.",
|
|
844
|
+
details: new System.Text.Json.Nodes.JsonObject
|
|
845
|
+
{
|
|
846
|
+
["reason"] = "max_commands",
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
var targets = new List<AuthoringTargetSummary>();
|
|
850
|
+
var effects = new List<string>();
|
|
851
|
+
var childRecords = new List<AuthoringChildPlanSummary>();
|
|
852
|
+
var aggregateUndo = AuthoringUndoLevel.Full;
|
|
853
|
+
var supportsSharedTransaction = true;
|
|
854
|
+
var hasMutatingChild = false;
|
|
855
|
+
var policy = AuthoringSafetyPolicyContext.Current;
|
|
856
|
+
if (policy == null)
|
|
857
|
+
{
|
|
858
|
+
throw new ToolCallControlException(
|
|
859
|
+
ToolCallErrorCodes.SafetyUnsupported,
|
|
860
|
+
"Batch planning requires the live authoring safety policy.",
|
|
861
|
+
details: new System.Text.Json.Nodes.JsonObject { ["reason"] = "policy_scope_missing" });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
var existingChildren = invocation.PlanBeingVerified?.ChildRecords;
|
|
865
|
+
if (existingChildren != null && existingChildren.Count != commands.Count)
|
|
866
|
+
{
|
|
867
|
+
throw new ToolCallControlException(
|
|
868
|
+
ToolCallErrorCodes.ConfirmationStale,
|
|
869
|
+
"The batch child confirmation records no longer match the command list.");
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
for (var index = 0; index < commands.Count; index++)
|
|
873
|
+
{
|
|
874
|
+
var command = commands[index];
|
|
875
|
+
var runner = FindRunner(command.Tool);
|
|
876
|
+
if (runner == null
|
|
877
|
+
|| string.Equals(command.Tool, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal))
|
|
878
|
+
{
|
|
879
|
+
throw new ToolCallControlException(
|
|
880
|
+
ToolCallErrorCodes.SafetyUnsupported,
|
|
881
|
+
"A batch child does not expose a supported authoring capability.",
|
|
882
|
+
details: new System.Text.Json.Nodes.JsonObject { ["reason"] = "child_capability_missing" });
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
var childContext = ToolCallContextNormalizer.DeriveChild(
|
|
886
|
+
invocation.Context,
|
|
887
|
+
new ToolCallControl
|
|
888
|
+
{
|
|
889
|
+
CallId = "batch-plan-" + index.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
|
890
|
+
},
|
|
891
|
+
requestId: "batch-plan-request-" + index.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
|
892
|
+
cancellationToken: invocation.Context.CancellationToken);
|
|
893
|
+
var childInvocation = policy.PrepareInvocation(new AuthoringInvocation(
|
|
894
|
+
childContext,
|
|
895
|
+
runner.Name,
|
|
896
|
+
command.Parameters,
|
|
897
|
+
runner));
|
|
898
|
+
var descriptor = childInvocation.Descriptor;
|
|
899
|
+
var risk = AuthoringSafetyPolicy.Classify(
|
|
900
|
+
childInvocation.Runner.ReadOnlyHint,
|
|
901
|
+
childInvocation.Runner.DestructiveHint,
|
|
902
|
+
descriptor);
|
|
903
|
+
if (descriptor != null
|
|
904
|
+
&& ((descriptor.SupportsPlanning && descriptor.Planner == null)
|
|
905
|
+
|| (descriptor.SupportsValidation && descriptor.Validator == null
|
|
906
|
+
&& descriptor.Planner == null)))
|
|
907
|
+
{
|
|
908
|
+
throw new ToolCallControlException(
|
|
909
|
+
ToolCallErrorCodes.SafetyUnsupported,
|
|
910
|
+
"A batch child has inconsistent authoring capability data.",
|
|
911
|
+
details: new System.Text.Json.Nodes.JsonObject
|
|
912
|
+
{
|
|
913
|
+
["reason"] = "binding_unavailable",
|
|
914
|
+
["stage"] = "declared_adapter_unavailable",
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
AuthoringValidationResult? childValidation = null;
|
|
919
|
+
if (descriptor?.Validator != null)
|
|
920
|
+
{
|
|
921
|
+
childValidation = descriptor.Validator.Validate(childInvocation);
|
|
922
|
+
if (childValidation == null || !childValidation.Valid)
|
|
923
|
+
{
|
|
924
|
+
var failureCode = childValidation?.FailureCode;
|
|
925
|
+
var safeFailureCode = string.IsNullOrWhiteSpace(failureCode)
|
|
926
|
+
? "validation_failed"
|
|
927
|
+
: failureCode ?? "validation_failed";
|
|
928
|
+
throw new ToolCallControlException(
|
|
929
|
+
safeFailureCode,
|
|
930
|
+
"A batch child did not pass validation.");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
var childPlan = descriptor?.Planner?.Plan(childInvocation);
|
|
935
|
+
if (descriptor?.Planner != null && childPlan == null)
|
|
936
|
+
{
|
|
937
|
+
throw new ToolCallControlException(
|
|
938
|
+
ToolCallErrorCodes.SafetyUnsupported,
|
|
939
|
+
"A batch child planner returned no plan.",
|
|
940
|
+
details: new System.Text.Json.Nodes.JsonObject
|
|
941
|
+
{
|
|
942
|
+
["reason"] = "binding_unavailable",
|
|
943
|
+
["stage"] = "planner_returned_null",
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
var validationTargets = childValidation?.Targets == null
|
|
948
|
+
? Array.Empty<AuthoringTargetSummary>()
|
|
949
|
+
: childValidation.Targets.Where(target => target != null).Take(8)
|
|
950
|
+
.Select(target => target.CloneSafe()).ToArray();
|
|
951
|
+
var plannedTargets = childPlan?.Targets == null
|
|
952
|
+
? Array.Empty<AuthoringTargetSummary>()
|
|
953
|
+
: childPlan.Targets.Where(target => target != null).Take(8)
|
|
954
|
+
.Select(target => target.CloneSafe()).ToArray();
|
|
955
|
+
IReadOnlyList<AuthoringTargetSummary> childTargets = plannedTargets.Length == 0
|
|
956
|
+
? validationTargets
|
|
957
|
+
: plannedTargets;
|
|
958
|
+
var plannedEffects = childPlan?.PredictedEffects == null
|
|
959
|
+
? Array.Empty<string>()
|
|
960
|
+
: childPlan.PredictedEffects.Where(effect => !string.IsNullOrWhiteSpace(effect)).Take(8)
|
|
961
|
+
.Select(effect => effect.Length <= 160 ? effect : effect.Substring(0, 160)).ToArray();
|
|
962
|
+
IReadOnlyList<string> childEffects = plannedEffects.Length > 0
|
|
963
|
+
? plannedEffects
|
|
964
|
+
: new[]
|
|
965
|
+
{
|
|
966
|
+
descriptor == null || descriptor.IsOpaque
|
|
967
|
+
? "undeclared"
|
|
968
|
+
: descriptor.MutationKind == AuthoringMutationKind.Unknown
|
|
969
|
+
? "authoring change"
|
|
970
|
+
: descriptor.MutationKind.ToString().ToLowerInvariant(),
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
AuthoringChildPlanSummary? existingAuthority = null;
|
|
974
|
+
if (existingChildren != null)
|
|
975
|
+
{
|
|
976
|
+
existingAuthority = existingChildren[index];
|
|
977
|
+
if (existingAuthority == null
|
|
978
|
+
|| existingAuthority.Index != index
|
|
979
|
+
|| !string.Equals(existingAuthority.ToolName, runner.Name, StringComparison.Ordinal))
|
|
980
|
+
{
|
|
981
|
+
throw new ToolCallControlException(
|
|
982
|
+
ToolCallErrorCodes.ConfirmationStale,
|
|
983
|
+
"The batch child confirmation records no longer match the command list.");
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
var childRecord = policy.CreateAggregateChildRecord(
|
|
988
|
+
index,
|
|
989
|
+
childInvocation,
|
|
990
|
+
childValidation,
|
|
991
|
+
childPlan,
|
|
992
|
+
childTargets,
|
|
993
|
+
childEffects,
|
|
994
|
+
existingAuthority);
|
|
995
|
+
childRecords.Add(childRecord);
|
|
996
|
+
|
|
997
|
+
if (risk != AuthoringRiskLevel.Read)
|
|
998
|
+
{
|
|
999
|
+
hasMutatingChild = true;
|
|
1000
|
+
aggregateUndo = (AuthoringUndoLevel)Math.Min(
|
|
1001
|
+
(int)aggregateUndo,
|
|
1002
|
+
(int)childRecord.UndoLevel);
|
|
1003
|
+
supportsSharedTransaction &= childRecord.SupportsSharedTransaction;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
targets.Add(new AuthoringTargetSummary
|
|
1007
|
+
{
|
|
1008
|
+
Kind = "batch-child",
|
|
1009
|
+
Name = runner.Name,
|
|
1010
|
+
Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(
|
|
1011
|
+
new Dictionary<string, JsonElement>
|
|
1012
|
+
{
|
|
1013
|
+
["index"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Index),
|
|
1014
|
+
["tool"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.ToolName),
|
|
1015
|
+
["argumentsHash"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.ArgumentsHash),
|
|
1016
|
+
["risk"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Risk),
|
|
1017
|
+
["undo"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Undo),
|
|
1018
|
+
["planLevel"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.PlanLevel),
|
|
1019
|
+
}),
|
|
1020
|
+
});
|
|
1021
|
+
targets.AddRange(childRecord.Targets.Select(target => target.CloneSafe()));
|
|
1022
|
+
effects.AddRange(childRecord.PredictedEffects);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (effects.Count == 0)
|
|
1026
|
+
effects.Add("authoring change");
|
|
1027
|
+
|
|
1028
|
+
return new AuthoringPlanSummary
|
|
1029
|
+
{
|
|
1030
|
+
UndoLevel = aggregateUndo,
|
|
1031
|
+
HasUndoOverride = true,
|
|
1032
|
+
SupportsSharedTransaction = hasMutatingChild && supportsSharedTransaction,
|
|
1033
|
+
Targets = targets.Take(32).Select(target => target.CloneSafe()).ToArray(),
|
|
1034
|
+
PredictedEffects = effects.Take(32).ToArray(),
|
|
1035
|
+
ChildRecords = childRecords.Take(Tool_Batch.HardMaxCommands)
|
|
1036
|
+
.Select(child => child.CloneBounded()).ToArray(),
|
|
1037
|
+
BeforeSummary = new System.Text.Json.Nodes.JsonObject
|
|
1038
|
+
{
|
|
1039
|
+
["childCount"] = commands.Count,
|
|
1040
|
+
},
|
|
1041
|
+
AfterSummary = new System.Text.Json.Nodes.JsonObject
|
|
1042
|
+
{
|
|
1043
|
+
["childCount"] = commands.Count,
|
|
1044
|
+
["predictedEffects"] = effects.Take(32).Aggregate(
|
|
1045
|
+
new System.Text.Json.Nodes.JsonArray(),
|
|
1046
|
+
(array, effect) =>
|
|
1047
|
+
{
|
|
1048
|
+
array.Add(effect);
|
|
1049
|
+
return array;
|
|
1050
|
+
}),
|
|
1051
|
+
},
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private sealed class ParsedCommand
|
|
1056
|
+
{
|
|
1057
|
+
public string Tool { get; }
|
|
1058
|
+
public Dictionary<string, JsonElement> Parameters { get; }
|
|
1059
|
+
|
|
1060
|
+
public ParsedCommand(string tool, Dictionary<string, JsonElement> parameters)
|
|
1061
|
+
{
|
|
1062
|
+
Tool = tool;
|
|
1063
|
+
Parameters = parameters;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
private static bool TryReadCommands(
|
|
1068
|
+
AuthoringInvocation invocation,
|
|
1069
|
+
out List<ParsedCommand> commands,
|
|
1070
|
+
out string? error)
|
|
1071
|
+
{
|
|
1072
|
+
commands = new List<ParsedCommand>();
|
|
1073
|
+
error = null;
|
|
1074
|
+
if (!TryGetArgument(invocation.Arguments, "commands", out var value)
|
|
1075
|
+
|| value.ValueKind != JsonValueKind.Array)
|
|
1076
|
+
{
|
|
1077
|
+
error = "validation_failed";
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
foreach (var item in value.EnumerateArray())
|
|
1082
|
+
{
|
|
1083
|
+
if (item.ValueKind != JsonValueKind.Object)
|
|
1084
|
+
{
|
|
1085
|
+
error = "validation_failed";
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
if (!TryGetProperty(item, "tool", out var toolValue)
|
|
1089
|
+
|| toolValue.ValueKind != JsonValueKind.String
|
|
1090
|
+
|| string.IsNullOrWhiteSpace(toolValue.GetString()))
|
|
1091
|
+
{
|
|
1092
|
+
error = "validation_failed";
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
var parameters = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
|
|
1097
|
+
if (TryGetProperty(item, "params", out var paramsValue)
|
|
1098
|
+
&& paramsValue.ValueKind != JsonValueKind.Null)
|
|
1099
|
+
{
|
|
1100
|
+
if (paramsValue.ValueKind != JsonValueKind.Object)
|
|
1101
|
+
{
|
|
1102
|
+
error = "validation_failed";
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
foreach (var property in paramsValue.EnumerateObject())
|
|
1106
|
+
parameters[property.Name] = property.Value.Clone();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
commands.Add(new ParsedCommand(toolValue.GetString()!, parameters));
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (commands.Count == 0)
|
|
1113
|
+
{
|
|
1114
|
+
error = "validation_failed";
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private static IRunTool? FindRunner(string name)
|
|
1121
|
+
{
|
|
1122
|
+
if (!UnityCopilotPluginEditor.HasInstance)
|
|
1123
|
+
return null;
|
|
1124
|
+
var manager = UnityCopilotPluginEditor.Instance.Tools;
|
|
1125
|
+
if (manager == null)
|
|
1126
|
+
return null;
|
|
1127
|
+
return manager.GetAllTools()
|
|
1128
|
+
.FirstOrDefault(item => item != null && string.Equals(item.Name, name, StringComparison.Ordinal));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private static bool TryGetArgument(
|
|
1132
|
+
IReadOnlyDictionary<string, JsonElement> arguments,
|
|
1133
|
+
string name,
|
|
1134
|
+
out JsonElement value)
|
|
1135
|
+
{
|
|
1136
|
+
if (arguments.TryGetValue(name, out value)) return true;
|
|
1137
|
+
foreach (var pair in arguments)
|
|
1138
|
+
{
|
|
1139
|
+
if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
|
|
1140
|
+
{
|
|
1141
|
+
value = pair.Value;
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
value = default;
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
private static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
|
|
1150
|
+
{
|
|
1151
|
+
foreach (var property in element.EnumerateObject())
|
|
1152
|
+
{
|
|
1153
|
+
if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
1154
|
+
{
|
|
1155
|
+
value = property.Value;
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
value = default;
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/// <summary>
|
|
1165
|
+
/// Read-only planner for the pilot. It performs the same live target
|
|
1166
|
+
/// inspection as the validator and adds predicted effects plus bounded
|
|
1167
|
+
/// before/after summaries. A plan for an invalid call is refused rather
|
|
1168
|
+
/// than issued, so a confirmation token can only ever describe an action
|
|
1169
|
+
/// whose targets were actually observed.
|
|
1170
|
+
/// </summary>
|
|
1171
|
+
public sealed class UnityPilotAuthoringPlanner : IAuthoringPlanner
|
|
1172
|
+
{
|
|
1173
|
+
public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
|
|
1174
|
+
{
|
|
1175
|
+
if (invocation == null)
|
|
1176
|
+
throw new ArgumentNullException(nameof(invocation));
|
|
1177
|
+
|
|
1178
|
+
var inspection = UnityPilotAuthoringInspection.Inspect(invocation);
|
|
1179
|
+
if (inspection.Errors.Count > 0)
|
|
1180
|
+
{
|
|
1181
|
+
throw new ToolCallControlException(
|
|
1182
|
+
"validation_failed",
|
|
1183
|
+
"The authoring call did not pass validation.",
|
|
1184
|
+
details: new System.Text.Json.Nodes.JsonObject
|
|
1185
|
+
{
|
|
1186
|
+
["reason"] = inspection.Errors[0],
|
|
1187
|
+
["errorCount"] = inspection.Errors.Count,
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
var kind = invocation.Descriptor?.MutationKind ?? AuthoringMutationKind.Unknown;
|
|
1192
|
+
var effects = inspection.Effects.Count == 0
|
|
1193
|
+
? new List<string> { kind == AuthoringMutationKind.Unknown ? "authoring change" : kind.ToString().ToLowerInvariant() }
|
|
1194
|
+
: inspection.Effects;
|
|
1195
|
+
return new AuthoringPlanSummary
|
|
1196
|
+
{
|
|
1197
|
+
Targets = inspection.Targets.Take(32).ToArray(),
|
|
1198
|
+
PredictedEffects = effects.Take(32).ToArray(),
|
|
1199
|
+
BeforeSummary = inspection.Before,
|
|
1200
|
+
AfterSummary = inspection.After,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/// <summary>Unity Undo-backed transaction factory attached to pilot runners.</summary>
|
|
1206
|
+
public sealed class UnityAuthoringTransactionFactory : IAuthoringTransactionFactory
|
|
1207
|
+
{
|
|
1208
|
+
public IAuthoringTransaction Begin(AuthoringInvocation invocation)
|
|
1209
|
+
{
|
|
1210
|
+
if (invocation == null)
|
|
1211
|
+
throw new ArgumentNullException(nameof(invocation));
|
|
1212
|
+
|
|
1213
|
+
return BeginStandalone(
|
|
1214
|
+
"Uco: " + invocation.Name,
|
|
1215
|
+
invocation.EffectiveUndoLevel
|
|
1216
|
+
?? invocation.Descriptor?.UndoLevel
|
|
1217
|
+
?? AuthoringUndoLevel.Partial);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/// <summary>
|
|
1221
|
+
/// Opens one named Unity Undo group and returns the transaction that
|
|
1222
|
+
/// owns it. Complete collapses the group to one user-visible step;
|
|
1223
|
+
/// Abort reverts only this group (never unrelated earlier history).
|
|
1224
|
+
/// </summary>
|
|
1225
|
+
public static IAuthoringTransaction BeginStandalone(string label, AuthoringUndoLevel advertisedUndo)
|
|
1226
|
+
{
|
|
1227
|
+
if (string.IsNullOrWhiteSpace(label))
|
|
1228
|
+
throw new ArgumentException("Transaction label must be non-empty.", nameof(label));
|
|
1229
|
+
|
|
1230
|
+
// The middleware may run on the transport thread; every Undo API
|
|
1231
|
+
// call is marshalled to the main thread (inline when already there).
|
|
1232
|
+
var group = MainThread.Instance.Run(() =>
|
|
1233
|
+
{
|
|
1234
|
+
Undo.IncrementCurrentGroup();
|
|
1235
|
+
var current = Undo.GetCurrentGroup();
|
|
1236
|
+
Undo.SetCurrentGroupName(label);
|
|
1237
|
+
return current;
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
return new UnityAuthoringTransaction(
|
|
1241
|
+
label,
|
|
1242
|
+
advertisedUndo,
|
|
1243
|
+
group,
|
|
1244
|
+
recordCreated: value => MainThread.Instance.Run(() => Undo.RegisterCreatedObjectUndo(
|
|
1245
|
+
RequireObject(value), label)),
|
|
1246
|
+
recordModified: (value, completeSnapshot) => MainThread.Instance.Run(() =>
|
|
1247
|
+
{
|
|
1248
|
+
var target = RequireObject(value);
|
|
1249
|
+
if (completeSnapshot)
|
|
1250
|
+
Undo.RegisterCompleteObjectUndo(target, label);
|
|
1251
|
+
else
|
|
1252
|
+
Undo.RecordObject(target, label);
|
|
1253
|
+
}),
|
|
1254
|
+
recordDeleted: value => MainThread.Instance.Run(() => Undo.RegisterCompleteObjectUndo(
|
|
1255
|
+
RequireObject(value), label)),
|
|
1256
|
+
complete: () => MainThread.Instance.Run(() => Undo.CollapseUndoOperations(group)),
|
|
1257
|
+
abort: () => MainThread.Instance.Run(() => Undo.RevertAllDownToGroup(group)));
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/// <summary>
|
|
1261
|
+
/// Test/tooling helper: opens a standalone transaction, installs it as
|
|
1262
|
+
/// the ambient scope, and completes it on dispose. Production calls
|
|
1263
|
+
/// always receive their scope from <see cref="AuthoringSafetyMiddleware"/>.
|
|
1264
|
+
/// </summary>
|
|
1265
|
+
public static IDisposable BeginStandaloneScope(string label, AuthoringUndoLevel advertisedUndo = AuthoringUndoLevel.Full)
|
|
1266
|
+
{
|
|
1267
|
+
var transaction = BeginStandalone(label, advertisedUndo);
|
|
1268
|
+
var scope = AuthoringTransactionScope.Push(transaction);
|
|
1269
|
+
return new StandaloneScope(transaction, scope);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
private sealed class StandaloneScope : IDisposable
|
|
1273
|
+
{
|
|
1274
|
+
private readonly IAuthoringTransaction _transaction;
|
|
1275
|
+
private readonly IDisposable _scope;
|
|
1276
|
+
private int _disposed;
|
|
1277
|
+
|
|
1278
|
+
public StandaloneScope(IAuthoringTransaction transaction, IDisposable scope)
|
|
1279
|
+
{
|
|
1280
|
+
_transaction = transaction;
|
|
1281
|
+
_scope = scope;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
public void Dispose()
|
|
1285
|
+
{
|
|
1286
|
+
if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0)
|
|
1287
|
+
return;
|
|
1288
|
+
try
|
|
1289
|
+
{
|
|
1290
|
+
_transaction.Complete();
|
|
1291
|
+
}
|
|
1292
|
+
finally
|
|
1293
|
+
{
|
|
1294
|
+
_scope.Dispose();
|
|
1295
|
+
_transaction.Dispose();
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
private static UnityEngine.Object RequireObject(object value)
|
|
1301
|
+
=> value as UnityEngine.Object
|
|
1302
|
+
?? throw new InvalidOperationException("The authoring target is not a Unity object.");
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/// <summary>
|
|
1306
|
+
/// Unity-aware transaction: affected-object summaries carry the object's
|
|
1307
|
+
/// type and name instead of the generic <c>ToString()</c> fallback, and
|
|
1308
|
+
/// hierarchy paths are reported as safe relative display values.
|
|
1309
|
+
/// </summary>
|
|
1310
|
+
public sealed class UnityAuthoringTransaction : AuthoringTransaction
|
|
1311
|
+
{
|
|
1312
|
+
public UnityAuthoringTransaction(
|
|
1313
|
+
string groupLabel,
|
|
1314
|
+
AuthoringUndoLevel advertisedUndo,
|
|
1315
|
+
int groupId,
|
|
1316
|
+
Action<object>? recordCreated,
|
|
1317
|
+
Action<object, bool>? recordModified,
|
|
1318
|
+
Action<object>? recordDeleted,
|
|
1319
|
+
Action? complete,
|
|
1320
|
+
Action? abort)
|
|
1321
|
+
: base(groupLabel, advertisedUndo, groupId, recordCreated, recordModified, recordDeleted, complete, abort)
|
|
1322
|
+
{
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
private readonly HashSet<string> _reported = new HashSet<string>(StringComparer.Ordinal);
|
|
1326
|
+
|
|
1327
|
+
protected override void AddAffected(object value)
|
|
1328
|
+
{
|
|
1329
|
+
if (value is UnityEngine.Object unityObject && unityObject != null)
|
|
1330
|
+
{
|
|
1331
|
+
// One object may be recorded more than once inside a call
|
|
1332
|
+
// (for example RegisterCreatedObjectUndo followed by a
|
|
1333
|
+
// complete snapshot); report it once.
|
|
1334
|
+
#if UNITY_6000_5_OR_NEWER
|
|
1335
|
+
var objectId = unityObject.GetEntityId().ToString();
|
|
1336
|
+
#else
|
|
1337
|
+
var objectId = unityObject.GetInstanceID().ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
1338
|
+
#endif
|
|
1339
|
+
var path = value is GameObject go
|
|
1340
|
+
? go.GetPath()
|
|
1341
|
+
: value is UnityEngine.Component component && component != null
|
|
1342
|
+
? component.gameObject.GetPath()
|
|
1343
|
+
: null;
|
|
1344
|
+
var summary = new AuthoringAffectedObjectSummary
|
|
1345
|
+
{
|
|
1346
|
+
Kind = value.GetType().Name,
|
|
1347
|
+
Name = unityObject.name,
|
|
1348
|
+
RelativePath = string.IsNullOrEmpty(path) ? null : path!.Replace('\\', '/').TrimStart('/'),
|
|
1349
|
+
};
|
|
1350
|
+
if (!_reported.Add(objectId))
|
|
1351
|
+
{
|
|
1352
|
+
AddAffectedEvent(summary);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
AddAffectedSummary(summary);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
base.AddAffected(value);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/// <summary>
|
|
1365
|
+
/// Small helper used by pilot tools. Every mutation records through the
|
|
1366
|
+
/// ambient transaction before changing Unity state.
|
|
1367
|
+
/// </summary>
|
|
1368
|
+
public static class UnityAuthoringUndo
|
|
1369
|
+
{
|
|
1370
|
+
/// <summary>
|
|
1371
|
+
/// Fails closed before the first mutation of a pilot tool body when
|
|
1372
|
+
/// the call did not arrive through the policy pipeline. An explicit
|
|
1373
|
+
/// ambient transaction (opened by the middleware, or deliberately by
|
|
1374
|
+
/// in-process tooling/tests) is accepted; a bare reflected runner
|
|
1375
|
+
/// call is not, so no object is created and then orphaned by a later
|
|
1376
|
+
/// missing-transaction failure.
|
|
1377
|
+
/// </summary>
|
|
1378
|
+
public static void RequireAuthoringScope()
|
|
1379
|
+
{
|
|
1380
|
+
if (AuthoringTransactionScope.Current != null)
|
|
1381
|
+
return;
|
|
1382
|
+
AuthoringPolicyApproval.Require();
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
public static void RecordCreated(UnityEngine.Object value)
|
|
1386
|
+
{
|
|
1387
|
+
var transaction = AuthoringTransactionScope.Current;
|
|
1388
|
+
if (transaction == null)
|
|
1389
|
+
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1390
|
+
transaction.RecordCreated(value);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
public static void RecordModified(UnityEngine.Object value, bool completeSnapshot = false)
|
|
1394
|
+
{
|
|
1395
|
+
var transaction = AuthoringTransactionScope.Current;
|
|
1396
|
+
if (transaction == null)
|
|
1397
|
+
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1398
|
+
transaction.RecordModified(value, completeSnapshot);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
public static void Destroy(UnityEngine.Object value)
|
|
1402
|
+
{
|
|
1403
|
+
var transaction = AuthoringTransactionScope.Current;
|
|
1404
|
+
if (transaction == null)
|
|
1405
|
+
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1406
|
+
transaction.RecordDeleted(value);
|
|
1407
|
+
Undo.DestroyObjectImmediate(value);
|
|
1408
|
+
transaction.MarkMutated();
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/// <summary>
|
|
1412
|
+
/// Undo-aware reparenting. <c>Undo.RecordObject</c> on a Transform
|
|
1413
|
+
/// does not capture hierarchy changes; Unity's
|
|
1414
|
+
/// <c>Undo.SetTransformParent</c> records the parent change inside
|
|
1415
|
+
/// the current group.
|
|
1416
|
+
/// </summary>
|
|
1417
|
+
public static void SetParent(Transform target, Transform? parent, bool worldPositionStays)
|
|
1418
|
+
{
|
|
1419
|
+
var transaction = AuthoringTransactionScope.Current;
|
|
1420
|
+
if (transaction == null)
|
|
1421
|
+
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1422
|
+
if (target == null)
|
|
1423
|
+
throw new ArgumentNullException(nameof(target));
|
|
1424
|
+
transaction.RecordHostRegistered(target);
|
|
1425
|
+
Undo.SetTransformParent(target, parent, worldPositionStays, "Uco: set parent");
|
|
1426
|
+
transaction.MarkMutated();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/// <summary>
|
|
1430
|
+
/// Undo-aware component creation. <c>Undo.AddComponent</c> creates
|
|
1431
|
+
/// and registers the component in one step; a second
|
|
1432
|
+
/// <c>RegisterCreatedObjectUndo</c> would duplicate the record.
|
|
1433
|
+
/// </summary>
|
|
1434
|
+
public static UnityEngine.Component? AddComponent(GameObject target, Type componentType)
|
|
1435
|
+
{
|
|
1436
|
+
var transaction = AuthoringTransactionScope.Current;
|
|
1437
|
+
if (transaction == null)
|
|
1438
|
+
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1439
|
+
if (target == null)
|
|
1440
|
+
throw new ArgumentNullException(nameof(target));
|
|
1441
|
+
if (componentType == null)
|
|
1442
|
+
throw new ArgumentNullException(nameof(componentType));
|
|
1443
|
+
|
|
1444
|
+
var component = Undo.AddComponent(target, componentType);
|
|
1445
|
+
if (component == null)
|
|
1446
|
+
return null;
|
|
1447
|
+
transaction.RecordHostRegistered(component);
|
|
1448
|
+
transaction.MarkMutated();
|
|
1449
|
+
return component;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/// <summary>
|
|
1453
|
+
/// Tracks an object that a Unity API already registered with Undo
|
|
1454
|
+
/// inside the current group (for example pasteboard duplication).
|
|
1455
|
+
/// </summary>
|
|
1456
|
+
public static void RecordHostRegistered(UnityEngine.Object value)
|
|
1457
|
+
{
|
|
1458
|
+
var transaction = AuthoringTransactionScope.Current;
|
|
1459
|
+
if (transaction == null)
|
|
1460
|
+
throw new InvalidOperationException("An authoring transaction is required.");
|
|
1461
|
+
transaction.RecordHostRegistered(value);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
public static void MarkMutated()
|
|
1465
|
+
=> AuthoringTransactionScope.Current?.MarkMutated();
|
|
1466
|
+
}
|
|
1467
|
+
}
|