@atelierai/uco 1.0.7 → 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.
Files changed (87) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +1 -1
  3. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
  4. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
  5. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
  6. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
  7. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
  8. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
  9. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
  10. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
  11. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
  12. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
  13. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
  14. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
  15. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
  16. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
  17. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +1034 -1034
  18. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
  19. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
  20. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
  21. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
  22. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
  23. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
  24. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
  25. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +20 -20
  26. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
  27. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
  28. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
  29. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
  30. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
  31. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
  32. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
  33. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
  34. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
  35. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
  36. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
  37. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
  38. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
  39. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
  40. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
  41. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
  42. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  43. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  44. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  45. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
  46. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
  47. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
  48. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
  49. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
  50. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +3 -3
  51. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
  52. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
  53. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
  54. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
  55. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +11 -11
  56. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
  57. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
  58. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
  59. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
  60. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +1 -1
  61. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
  62. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
  63. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
  64. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
  65. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
  66. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
  67. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
  68. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
  69. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
  70. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
  71. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
  72. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
  73. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
  74. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
  75. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
  76. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
  77. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
  78. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
  79. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
  80. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
  81. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
  82. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
  83. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
  84. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
  85. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
  86. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  87. /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.6";
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
+ }