@atelierai/uco 1.0.7 → 1.0.9

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +29 -8
  2. package/package.json +2 -2
  3. package/vendor/plugin/com.atelierai.unity.copilot/CHANGELOG.md +4 -4
  4. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
  5. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
  6. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
  7. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
  8. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
  9. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
  10. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
  11. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
  12. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
  13. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
  14. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
  15. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
  16. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
  17. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/ToolGroups.SetEnabled.cs +92 -92
  18. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
  19. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +887 -1034
  20. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
  21. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
  22. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
  23. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
  24. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
  25. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
  26. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
  27. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +22 -22
  28. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
  29. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
  30. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
  31. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
  32. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
  33. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
  34. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
  35. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
  36. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
  37. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
  38. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
  39. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
  40. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolGroupRegistry.cs +461 -460
  41. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
  42. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
  43. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
  44. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
  45. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotPromptsWindow.uss +11 -11
  46. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotResourcesWindow.uss +16 -16
  47. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotToolsWindow.uss +36 -36
  48. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/MainWindow.uss +183 -183
  49. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss → _copilot-list-window.uss} +18 -18
  50. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/_list-view.uss +77 -77
  51. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotPromptsWindow.uxml +18 -18
  52. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotResourcesWindow.uxml +18 -18
  53. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotToolsWindow.uxml +18 -18
  54. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/MainWindow.uxml +115 -115
  55. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  56. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  57. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  58. package/vendor/plugin/com.atelierai.unity.copilot/README.md +88 -88
  59. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
  60. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
  61. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
  62. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
  63. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
  64. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +3 -3
  65. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
  66. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
  67. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
  68. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
  69. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +11 -11
  70. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
  71. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
  72. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
  73. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorDevelopmentDependencyTests.cs +148 -148
  74. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorFlatLayoutTests.cs +166 -166
  75. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetInstallManifestTests.cs +436 -436
  76. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
  77. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetPackageInstallerStaleVersionCleanupTests.cs +200 -200
  78. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +2 -2
  79. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
  80. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
  81. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
  82. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
  83. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
  84. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
  85. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
  86. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
  87. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
  88. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
  89. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
  90. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
  91. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
  92. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
  93. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +199 -199
  94. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
  95. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
  96. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
  97. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
  98. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
  99. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
  100. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
  101. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
  102. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
  103. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
  104. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
  105. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  106. /package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss.meta → _copilot-list-window.uss.meta} +0 -0
  107. /package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs.meta → UcoPluginTests.cs.meta} +0 -0
@@ -1,295 +1,295 @@
1
- /*
2
- ┌──────────────────────────────────────────────────────────────────┐
3
- │ Author: Ivan Murzak (https://github.com/IvanMurzak) │
4
- │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
5
- │ Copyright (c) 2025 Ivan Murzak │
6
- │ Licensed under the Apache License, Version 2.0. │
7
- │ See the LICENSE file in the project root for more information. │
8
- └──────────────────────────────────────────────────────────────────┘
9
- */
10
-
11
- #nullable enable
12
-
13
- using System;
14
- using System.Threading;
15
- using System.Threading.Tasks;
16
- using com.AtelierAI.Uco.Framework;
17
- using WsState = com.AtelierAI.Uco.Framework.ConnectionState;
18
- using com.AtelierAI.Uco.Framework.Common.Model;
19
- using com.IvanMurzak.ReflectorNet;
20
- using Microsoft.Extensions.Logging;
21
- using R3;
22
-
23
- namespace com.AtelierAI.Unity.Copilot
24
- {
25
- using LogLevel = Runtime.Utils.LogLevel;
26
-
27
- public partial class UnityCopilotPlugin : IDisposable
28
- {
29
- public const string Version = "1.0.5";
30
-
31
- private static int _singletonCount = 0;
32
- public static bool HasAnyInstance => _singletonCount > 0;
33
- protected static void IncrementSingletonCount() => Interlocked.Increment(ref _singletonCount);
34
- protected static void DecrementSingletonCount() => Interlocked.Decrement(ref _singletonCount);
35
-
36
- private static LogLevel _configuredLogLevel = LogLevel.Warning;
37
- // Uses direct enum comparison: configured threshold <= requested level means enabled
38
- public static bool IsLogEnabled(LogLevel level) => _configuredLogLevel <= level;
39
- public static void ApplyLogLevel(LogLevel level) => _configuredLogLevel = level;
40
-
41
- protected readonly CompositeDisposable _disposables = new();
42
- protected readonly CopilotPluginSlot _plugin = new();
43
-
44
- // Tracks only the latest plugin's ConnectionState subscription.
45
- // Replaced (old one disposed) each time BuildMcpPlugin creates a new IUcoPlugin instance.
46
- private IDisposable? _pluginConnectionSubscription;
47
-
48
- public IUcoPlugin? UcoPluginInstance => _plugin.Instance;
49
- public bool HasMcpPluginInstance => _plugin.HasInstance;
50
-
51
- public ILogger Logger => UcoPluginInstance?.Logger ?? _logger;
52
- public Reflector? Reflector => UcoPluginInstance?.UcoManager.Reflector;
53
- public IToolManager? Tools => UcoPluginInstance?.UcoManager.ToolManager;
54
- public IPromptManager? Prompts => UcoPluginInstance?.UcoManager.PromptManager;
55
- public IResourceManager? Resources => UcoPluginInstance?.UcoManager.ResourceManager;
56
-
57
- public UnityLogCollector? LogCollector { get; protected set; } = null;
58
-
59
- // --- Connection state ---
60
-
61
- protected readonly ReactiveProperty<WsState> _connectionState
62
- = new(WsState.Disconnected);
63
-
64
- public ReadOnlyReactiveProperty<WsState> ConnectionState => _connectionState;
65
-
66
- public ReadOnlyReactiveProperty<bool> IsConnected => _connectionState
67
- .Select(x => x == WsState.Connected)
68
- .ToReadOnlyReactiveProperty(false);
69
-
70
- // --- Constructor / Dispose ---
71
-
72
- protected UnityCopilotPlugin()
73
- {
74
- _disposables.Add(_connectionState);
75
- }
76
-
77
- public virtual void Dispose()
78
- {
79
- _pluginConnectionSubscription?.Dispose();
80
- _pluginConnectionSubscription = null;
81
- _disposables.Dispose();
82
- // LogCollector and _connectionState are disposed by _disposables
83
- LogCollector = null;
84
- }
85
-
86
- // --- Log collector ---
87
-
88
- public void AddUnityLogCollector(ILogStorage logStorage)
89
- {
90
- if (logStorage == null)
91
- throw new ArgumentNullException(nameof(logStorage));
92
-
93
- if (LogCollector != null)
94
- throw new InvalidOperationException($"{nameof(UnityLogCollector)} is already added.");
95
-
96
- LogCollector = new UnityLogCollector(logStorage);
97
- _disposables.Add(LogCollector);
98
- }
99
-
100
- public void AddUnityLogCollectorIfNeeded(Func<ILogStorage> logStorageProvider)
101
- {
102
- if (LogCollector != null)
103
- return;
104
-
105
- AddUnityLogCollector(logStorageProvider());
106
- }
107
-
108
- public void DisposeLogCollector()
109
- {
110
- LogCollector?.Save();
111
- LogCollector?.Dispose();
112
- LogCollector = null;
113
- }
114
-
115
- // --- Connection methods ---
116
-
117
- public Task<bool> ConnectIfNeeded(CancellationToken cancellationToken = default)
118
- {
119
- if (!unityConnectionConfig.KeepConnected)
120
- return Task.FromResult(false);
121
- return ConnectCore(restartPendingAttempt: false, cancellationToken);
122
- }
123
-
124
- /// <summary>
125
- /// Starts an explicit connection attempt. If an earlier attempt is still pending while
126
- /// the plugin is disconnected, it is cancelled first so the new attempt does not merely
127
- /// wait behind stale connection work.
128
- /// </summary>
129
- public Task<bool> Connect(CancellationToken cancellationToken = default)
130
- => ConnectCore(restartPendingAttempt: true, cancellationToken);
131
-
132
- private async Task<bool> ConnectCore(
133
- bool restartPendingAttempt,
134
- CancellationToken cancellationToken)
135
- {
136
- _logger.LogTrace("{method} called.", nameof(Connect));
137
- try
138
- {
139
- var mcpPlugin = UcoPluginInstance;
140
- if (mcpPlugin == null)
141
- {
142
- _logger.LogError("{method}: uco plugin instance is null.", nameof(Connect));
143
- return false;
144
- }
145
-
146
- if (restartPendingAttempt
147
- && mcpPlugin.ConnectionState.CurrentValue != WsState.Connected)
148
- {
149
- // ConnectionManager cancellation happens before its bounded synchronous
150
- // cleanup, so the old attempt releases its gate while the new call waits
151
- // with the caller-provided cancellation token.
152
- mcpPlugin.DisconnectImmediate();
153
- }
154
-
155
- return await mcpPlugin.Connect(cancellationToken);
156
- }
157
- finally
158
- {
159
- _logger.LogTrace("{method} completed.", nameof(Connect));
160
- }
161
- }
162
-
163
- public async Task Disconnect()
164
- {
165
- _logger.LogTrace("{method} called.", nameof(Disconnect));
166
- try
167
- {
168
- var mcpPlugin = UcoPluginInstance;
169
- if (mcpPlugin == null)
170
- {
171
- _logger.LogWarning("{method}: uco plugin instance is null, nothing to disconnect, ignoring.",
172
- nameof(Disconnect));
173
- return;
174
- }
175
- try
176
- {
177
- _logger.LogDebug("{method}: Disconnecting uco plugin instance.", nameof(Disconnect));
178
- await mcpPlugin.Disconnect();
179
- }
180
- catch (Exception e)
181
- {
182
- _logger.LogError("{method}: Exception during disconnecting: {exception}",
183
- nameof(Disconnect), e);
184
- }
185
- }
186
- finally
187
- {
188
- _logger.LogTrace("{method} completed.", nameof(Disconnect));
189
- }
190
- }
191
-
192
- public void DisconnectImmediate()
193
- {
194
- _logger.LogTrace("{method} called.", nameof(DisconnectImmediate));
195
- try
196
- {
197
- var mcpPlugin = UcoPluginInstance;
198
- if (mcpPlugin == null)
199
- {
200
- _logger.LogWarning("{method}: uco plugin instance is null, nothing to disconnect, ignoring.",
201
- nameof(DisconnectImmediate));
202
- return;
203
- }
204
- try
205
- {
206
- _logger.LogDebug("{method}: Disconnecting uco plugin instance.", nameof(DisconnectImmediate));
207
- mcpPlugin.DisconnectImmediate();
208
- }
209
- catch (Exception e)
210
- {
211
- _logger.LogError("{method}: Exception during disconnecting: {exception}",
212
- nameof(DisconnectImmediate), e);
213
- }
214
- }
215
- finally
216
- {
217
- _logger.LogTrace("{method} completed.", nameof(DisconnectImmediate));
218
- }
219
- }
220
-
221
- public async Task NotifyToolRequestCompleted(RequestToolCompletedData request, CancellationToken cancellationToken = default)
222
- {
223
- var mcpPlugin = UcoPluginInstance
224
- ?? throw new InvalidOperationException($"{nameof(UcoPluginInstance)} is null");
225
-
226
- while (mcpPlugin.ConnectionState.CurrentValue != WsState.Connected)
227
- {
228
- await Task.Delay(100, cancellationToken);
229
- if (cancellationToken.IsCancellationRequested)
230
- {
231
- _logger.LogWarning("{method}: operation cancelled while waiting for connection.",
232
- nameof(NotifyToolRequestCompleted));
233
- return;
234
- }
235
- }
236
-
237
- if (mcpPlugin.UcoManager == null)
238
- {
239
- _logger.LogCritical("{method}: {instance} is null",
240
- nameof(NotifyToolRequestCompleted), nameof(mcpPlugin.UcoManager));
241
- return;
242
- }
243
-
244
- if (mcpPlugin.UcoManagerHub == null)
245
- {
246
- _logger.LogCritical("{method}: {instance} is null",
247
- nameof(NotifyToolRequestCompleted), nameof(mcpPlugin.UcoManagerHub));
248
- return;
249
- }
250
-
251
- await mcpPlugin.UcoManagerHub.NotifyToolRequestCompleted(request);
252
- }
253
-
254
- // --- Token / Port utilities ---
255
-
256
- /// <summary>
257
- /// Generates a cryptographically random URL-safe token.
258
- /// Used by <see cref="UnityConnectionConfig.SetDefault"/> for initial token generation.
259
- /// </summary>
260
- public static string GenerateToken()
261
- {
262
- var bytes = new byte[32];
263
- using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
264
- rng.GetBytes(bytes);
265
- return Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
266
- }
267
-
268
- /// <summary>
269
- /// Generate a deterministic TCP port based on current directory.
270
- /// Uses SHA256 hash for better distribution and less collisions.
271
- /// Port range: 20000-29999 (avoids Windows ephemeral/reserved port ranges).
272
- /// </summary>
273
- public static int GeneratePortFromDirectory()
274
- {
275
- const int MinPort = 20000; // Range chosen to avoid Windows ephemeral/reserved ports (49152-65535)
276
- const int MaxPort = 29999;
277
- const int PortRange = MaxPort - MinPort + 1;
278
-
279
- var currentDir = Environment.CurrentDirectory.ToLowerInvariant();
280
-
281
- using (var sha256 = System.Security.Cryptography.SHA256.Create())
282
- {
283
- var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(currentDir));
284
-
285
- // Use first 4 bytes as an unsigned integer to avoid Math.Abs(int.MinValue) overflow
286
- var hash = (uint)BitConverter.ToInt32(hashBytes, 0);
287
-
288
- // Map to port range
289
- var port = MinPort + (int)(hash % PortRange);
290
-
291
- return port;
292
- }
293
- }
294
- }
295
- }
1
+ /*
2
+ ┌──────────────────────────────────────────────────────────────────┐
3
+ │ Author: Ivan Murzak (https://github.com/IvanMurzak) │
4
+ │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
5
+ │ Copyright (c) 2025 Ivan Murzak │
6
+ │ Licensed under the Apache License, Version 2.0. │
7
+ │ See the LICENSE file in the project root for more information. │
8
+ └──────────────────────────────────────────────────────────────────┘
9
+ */
10
+
11
+ #nullable enable
12
+
13
+ using System;
14
+ using System.Threading;
15
+ using System.Threading.Tasks;
16
+ using com.AtelierAI.Uco.Framework;
17
+ using WsState = com.AtelierAI.Uco.Framework.ConnectionState;
18
+ using com.AtelierAI.Uco.Framework.Common.Model;
19
+ using com.IvanMurzak.ReflectorNet;
20
+ using Microsoft.Extensions.Logging;
21
+ using R3;
22
+
23
+ namespace com.AtelierAI.Unity.Copilot
24
+ {
25
+ using LogLevel = Runtime.Utils.LogLevel;
26
+
27
+ public partial class UnityCopilotPlugin : IDisposable
28
+ {
29
+ public const string Version = "1.0.7";
30
+
31
+ private static int _singletonCount = 0;
32
+ public static bool HasAnyInstance => _singletonCount > 0;
33
+ protected static void IncrementSingletonCount() => Interlocked.Increment(ref _singletonCount);
34
+ protected static void DecrementSingletonCount() => Interlocked.Decrement(ref _singletonCount);
35
+
36
+ private static LogLevel _configuredLogLevel = LogLevel.Warning;
37
+ // Uses direct enum comparison: configured threshold <= requested level means enabled
38
+ public static bool IsLogEnabled(LogLevel level) => _configuredLogLevel <= level;
39
+ public static void ApplyLogLevel(LogLevel level) => _configuredLogLevel = level;
40
+
41
+ protected readonly CompositeDisposable _disposables = new();
42
+ protected readonly CopilotPluginSlot _plugin = new();
43
+
44
+ // Tracks only the latest plugin's ConnectionState subscription.
45
+ // Replaced (old one disposed) each time BuildUcoPlugin creates a new IUcoPlugin instance.
46
+ private IDisposable? _pluginConnectionSubscription;
47
+
48
+ public IUcoPlugin? UcoPluginInstance => _plugin.Instance;
49
+ public bool HasUcoPluginInstance => _plugin.HasInstance;
50
+
51
+ public ILogger Logger => UcoPluginInstance?.Logger ?? _logger;
52
+ public Reflector? Reflector => UcoPluginInstance?.UcoManager.Reflector;
53
+ public IToolManager? Tools => UcoPluginInstance?.UcoManager.ToolManager;
54
+ public IPromptManager? Prompts => UcoPluginInstance?.UcoManager.PromptManager;
55
+ public IResourceManager? Resources => UcoPluginInstance?.UcoManager.ResourceManager;
56
+
57
+ public UnityLogCollector? LogCollector { get; protected set; } = null;
58
+
59
+ // --- Connection state ---
60
+
61
+ protected readonly ReactiveProperty<WsState> _connectionState
62
+ = new(WsState.Disconnected);
63
+
64
+ public ReadOnlyReactiveProperty<WsState> ConnectionState => _connectionState;
65
+
66
+ public ReadOnlyReactiveProperty<bool> IsConnected => _connectionState
67
+ .Select(x => x == WsState.Connected)
68
+ .ToReadOnlyReactiveProperty(false);
69
+
70
+ // --- Constructor / Dispose ---
71
+
72
+ protected UnityCopilotPlugin()
73
+ {
74
+ _disposables.Add(_connectionState);
75
+ }
76
+
77
+ public virtual void Dispose()
78
+ {
79
+ _pluginConnectionSubscription?.Dispose();
80
+ _pluginConnectionSubscription = null;
81
+ _disposables.Dispose();
82
+ // LogCollector and _connectionState are disposed by _disposables
83
+ LogCollector = null;
84
+ }
85
+
86
+ // --- Log collector ---
87
+
88
+ public void AddUnityLogCollector(ILogStorage logStorage)
89
+ {
90
+ if (logStorage == null)
91
+ throw new ArgumentNullException(nameof(logStorage));
92
+
93
+ if (LogCollector != null)
94
+ throw new InvalidOperationException($"{nameof(UnityLogCollector)} is already added.");
95
+
96
+ LogCollector = new UnityLogCollector(logStorage);
97
+ _disposables.Add(LogCollector);
98
+ }
99
+
100
+ public void AddUnityLogCollectorIfNeeded(Func<ILogStorage> logStorageProvider)
101
+ {
102
+ if (LogCollector != null)
103
+ return;
104
+
105
+ AddUnityLogCollector(logStorageProvider());
106
+ }
107
+
108
+ public void DisposeLogCollector()
109
+ {
110
+ LogCollector?.Save();
111
+ LogCollector?.Dispose();
112
+ LogCollector = null;
113
+ }
114
+
115
+ // --- Connection methods ---
116
+
117
+ public Task<bool> ConnectIfNeeded(CancellationToken cancellationToken = default)
118
+ {
119
+ if (!unityConnectionConfig.KeepConnected)
120
+ return Task.FromResult(false);
121
+ return ConnectCore(restartPendingAttempt: false, cancellationToken);
122
+ }
123
+
124
+ /// <summary>
125
+ /// Starts an explicit connection attempt. If an earlier attempt is still pending while
126
+ /// the plugin is disconnected, it is cancelled first so the new attempt does not merely
127
+ /// wait behind stale connection work.
128
+ /// </summary>
129
+ public Task<bool> Connect(CancellationToken cancellationToken = default)
130
+ => ConnectCore(restartPendingAttempt: true, cancellationToken);
131
+
132
+ private async Task<bool> ConnectCore(
133
+ bool restartPendingAttempt,
134
+ CancellationToken cancellationToken)
135
+ {
136
+ _logger.LogTrace("{method} called.", nameof(Connect));
137
+ try
138
+ {
139
+ var ucoPlugin = UcoPluginInstance;
140
+ if (ucoPlugin == null)
141
+ {
142
+ _logger.LogError("{method}: uco plugin instance is null.", nameof(Connect));
143
+ return false;
144
+ }
145
+
146
+ if (restartPendingAttempt
147
+ && ucoPlugin.ConnectionState.CurrentValue != WsState.Connected)
148
+ {
149
+ // ConnectionManager cancellation happens before its bounded synchronous
150
+ // cleanup, so the old attempt releases its gate while the new call waits
151
+ // with the caller-provided cancellation token.
152
+ ucoPlugin.DisconnectImmediate();
153
+ }
154
+
155
+ return await ucoPlugin.Connect(cancellationToken);
156
+ }
157
+ finally
158
+ {
159
+ _logger.LogTrace("{method} completed.", nameof(Connect));
160
+ }
161
+ }
162
+
163
+ public async Task Disconnect()
164
+ {
165
+ _logger.LogTrace("{method} called.", nameof(Disconnect));
166
+ try
167
+ {
168
+ var ucoPlugin = UcoPluginInstance;
169
+ if (ucoPlugin == null)
170
+ {
171
+ _logger.LogWarning("{method}: uco plugin instance is null, nothing to disconnect, ignoring.",
172
+ nameof(Disconnect));
173
+ return;
174
+ }
175
+ try
176
+ {
177
+ _logger.LogDebug("{method}: Disconnecting uco plugin instance.", nameof(Disconnect));
178
+ await ucoPlugin.Disconnect();
179
+ }
180
+ catch (Exception e)
181
+ {
182
+ _logger.LogError("{method}: Exception during disconnecting: {exception}",
183
+ nameof(Disconnect), e);
184
+ }
185
+ }
186
+ finally
187
+ {
188
+ _logger.LogTrace("{method} completed.", nameof(Disconnect));
189
+ }
190
+ }
191
+
192
+ public void DisconnectImmediate()
193
+ {
194
+ _logger.LogTrace("{method} called.", nameof(DisconnectImmediate));
195
+ try
196
+ {
197
+ var ucoPlugin = UcoPluginInstance;
198
+ if (ucoPlugin == null)
199
+ {
200
+ _logger.LogWarning("{method}: uco plugin instance is null, nothing to disconnect, ignoring.",
201
+ nameof(DisconnectImmediate));
202
+ return;
203
+ }
204
+ try
205
+ {
206
+ _logger.LogDebug("{method}: Disconnecting uco plugin instance.", nameof(DisconnectImmediate));
207
+ ucoPlugin.DisconnectImmediate();
208
+ }
209
+ catch (Exception e)
210
+ {
211
+ _logger.LogError("{method}: Exception during disconnecting: {exception}",
212
+ nameof(DisconnectImmediate), e);
213
+ }
214
+ }
215
+ finally
216
+ {
217
+ _logger.LogTrace("{method} completed.", nameof(DisconnectImmediate));
218
+ }
219
+ }
220
+
221
+ public async Task NotifyToolRequestCompleted(RequestToolCompletedData request, CancellationToken cancellationToken = default)
222
+ {
223
+ var ucoPlugin = UcoPluginInstance
224
+ ?? throw new InvalidOperationException($"{nameof(UcoPluginInstance)} is null");
225
+
226
+ while (ucoPlugin.ConnectionState.CurrentValue != WsState.Connected)
227
+ {
228
+ await Task.Delay(100, cancellationToken);
229
+ if (cancellationToken.IsCancellationRequested)
230
+ {
231
+ _logger.LogWarning("{method}: operation cancelled while waiting for connection.",
232
+ nameof(NotifyToolRequestCompleted));
233
+ return;
234
+ }
235
+ }
236
+
237
+ if (ucoPlugin.UcoManager == null)
238
+ {
239
+ _logger.LogCritical("{method}: {instance} is null",
240
+ nameof(NotifyToolRequestCompleted), nameof(ucoPlugin.UcoManager));
241
+ return;
242
+ }
243
+
244
+ if (ucoPlugin.UcoManagerHub == null)
245
+ {
246
+ _logger.LogCritical("{method}: {instance} is null",
247
+ nameof(NotifyToolRequestCompleted), nameof(ucoPlugin.UcoManagerHub));
248
+ return;
249
+ }
250
+
251
+ await ucoPlugin.UcoManagerHub.NotifyToolRequestCompleted(request);
252
+ }
253
+
254
+ // --- Token / Port utilities ---
255
+
256
+ /// <summary>
257
+ /// Generates a cryptographically random URL-safe token.
258
+ /// Used by <see cref="UnityConnectionConfig.SetDefault"/> for initial token generation.
259
+ /// </summary>
260
+ public static string GenerateToken()
261
+ {
262
+ var bytes = new byte[32];
263
+ using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
264
+ rng.GetBytes(bytes);
265
+ return Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
266
+ }
267
+
268
+ /// <summary>
269
+ /// Generate a deterministic TCP port based on current directory.
270
+ /// Uses SHA256 hash for better distribution and less collisions.
271
+ /// Port range: 20000-29999 (avoids Windows ephemeral/reserved port ranges).
272
+ /// </summary>
273
+ public static int GeneratePortFromDirectory()
274
+ {
275
+ const int MinPort = 20000; // Range chosen to avoid Windows ephemeral/reserved ports (49152-65535)
276
+ const int MaxPort = 29999;
277
+ const int PortRange = MaxPort - MinPort + 1;
278
+
279
+ var currentDir = Environment.CurrentDirectory.ToLowerInvariant();
280
+
281
+ using (var sha256 = System.Security.Cryptography.SHA256.Create())
282
+ {
283
+ var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(currentDir));
284
+
285
+ // Use first 4 bytes as an unsigned integer to avoid Math.Abs(int.MinValue) overflow
286
+ var hash = (uint)BitConverter.ToInt32(hashBytes, 0);
287
+
288
+ // Map to port range
289
+ var port = MinPort + (int)(hash % PortRange);
290
+
291
+ return port;
292
+ }
293
+ }
294
+ }
295
+ }