@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.
- package/CHANGELOG.md +11 -0
- package/package.json +1 -1
- 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/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 +20 -20
- 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 +3 -3
- 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 +11 -11
- 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 +1 -1
- 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/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.
|
|
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
|
|
46
|
-
private IDisposable? _pluginConnectionSubscription;
|
|
47
|
-
|
|
48
|
-
public IUcoPlugin? UcoPluginInstance => _plugin.Instance;
|
|
49
|
-
public bool
|
|
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
|
|
140
|
-
if (
|
|
141
|
-
{
|
|
142
|
-
_logger.LogError("{method}: uco plugin instance is null.", nameof(Connect));
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (restartPendingAttempt
|
|
147
|
-
&&
|
|
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
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return await
|
|
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
|
|
169
|
-
if (
|
|
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
|
|
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
|
|
198
|
-
if (
|
|
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
|
-
|
|
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
|
|
224
|
-
?? throw new InvalidOperationException($"{nameof(UcoPluginInstance)} is null");
|
|
225
|
-
|
|
226
|
-
while (
|
|
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 (
|
|
238
|
-
{
|
|
239
|
-
_logger.LogCritical("{method}: {instance} is null",
|
|
240
|
-
nameof(NotifyToolRequestCompleted), nameof(
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
{
|
|
246
|
-
_logger.LogCritical("{method}: {instance} is null",
|
|
247
|
-
nameof(NotifyToolRequestCompleted), nameof(
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
await
|
|
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
|
+
}
|