@atelierai/uco 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -8
- package/package.json +2 -2
- package/vendor/plugin/com.atelierai.unity.copilot/CHANGELOG.md +4 -4
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/ToolGroups.SetEnabled.cs +92 -92
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +0 -147
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +16 -0
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +3 -3
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolGroupRegistry.cs +461 -460
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotPromptsWindow.uss +11 -11
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotResourcesWindow.uss +16 -16
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotToolsWindow.uss +36 -36
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/MainWindow.uss +183 -183
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss → _copilot-list-window.uss} +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/_list-view.uss +77 -77
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotPromptsWindow.uxml +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotResourcesWindow.uxml +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotToolsWindow.uxml +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/MainWindow.uxml +115 -115
- 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/README.md +88 -88
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/BufferedFileLogStorage.cs +208 -207
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +3 -1
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/UnityLogCollector.cs +282 -280
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +1 -1
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorDevelopmentDependencyTests.cs +148 -148
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorFlatLayoutTests.cs +166 -166
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetInstallManifestTests.cs +436 -436
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +1 -1
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetPackageInstallerStaleVersionCleanupTests.cs +200 -200
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +1 -1
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +199 -199
- package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
- /package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss.meta → _copilot-list-window.uss.meta} +0 -0
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<h1>Unity Copilot</h1>
|
|
3
|
-
<p>Drive the Unity Editor from any AI agent over plain REST + WebSocket —
|
|
4
|
-
<p><code>com.atelierai.unity.copilot</code></p>
|
|
5
|
-
</div>
|
|
6
|
-
|
|
7
|
-
Unity Copilot is a Unity Editor/Runtime plugin that exposes the Editor as an
|
|
8
|
-
HTTP service: 160+ tools covering scenes, GameObjects, components, assets,
|
|
9
|
-
prefabs, scripts, packages, screenshots, console, tests, and builds — plus
|
|
10
|
-
prompts and resources. A self-hosted Node bridge connects the Editor to the
|
|
11
|
-
outside world; the **uco** CLI drives the whole surface from any agent or
|
|
12
|
-
terminal.
|
|
13
|
-
|
|
14
|
-
## Highlights
|
|
15
|
-
|
|
16
|
-
- **160+ Editor tools** — authoring, code (including Roslyn compile-and-execute
|
|
17
|
-
with confirmation gating for risky operations), diagnostics, build & tests,
|
|
18
|
-
visuals, physics
|
|
19
|
-
- **Three-surface AI skills** — `uco setup-skills` generates focused skill
|
|
20
|
-
bundles (setup / lifecycle / live-editor) for Claude Code, Codex, Cursor,
|
|
21
|
-
and other agents, refreshed from the live tool catalog
|
|
22
|
-
- **Deterministic, safe connectivity** — one port per project (hashed from the
|
|
23
|
-
project path), bearer-token auth, localhost by default, no vendor lock-in:
|
|
24
|
-
anything that can speak HTTP or run a CLI can drive the Editor
|
|
25
|
-
- **Durable calls** — async invocations return a call id queryable later
|
|
26
|
-
(`uco call get <id>`); structured error envelopes survive the whole stack
|
|
27
|
-
- **Dual-editor support** — gates run against Unity 2022.3 LTS and 6000.5
|
|
28
|
-
|
|
29
|
-
## Requirements
|
|
30
|
-
|
|
31
|
-
- Unity **2022.3 or newer** (tested on 2022.3 LTS and 6000.5)
|
|
32
|
-
- **Node.js 20+** for the bridge server (the plugin auto-discovers the `uco`
|
|
33
|
-
npm package — global or project-local — or set `nodeServerPath` in the
|
|
34
|
-
config to point at a specific install)
|
|
35
|
-
|
|
36
|
-
## Install
|
|
37
|
-
|
|
38
|
-
### Via the uco CLI (recommended)
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm install -g @atelierai/uco
|
|
42
|
-
uco install <path-to-your-unity-project>
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
This embeds the plugin package, stages the framework DLLs, writes the initial
|
|
46
|
-
config, and can generate agent skills and wrappers (`uco setup-skills`).
|
|
47
|
-
|
|
48
|
-
### Via OpenUPM
|
|
49
|
-
|
|
50
|
-
Add the scoped registry to `Packages/manifest.json` and reference the package:
|
|
51
|
-
|
|
52
|
-
```json
|
|
53
|
-
{
|
|
54
|
-
"dependencies": {
|
|
55
|
-
"com.atelierai.unity.copilot": "1.0.0"
|
|
56
|
-
},
|
|
57
|
-
"scopedRegistries": [
|
|
58
|
-
{
|
|
59
|
-
"name": "package.openupm.com",
|
|
60
|
-
"url": "https://package.openupm.com",
|
|
61
|
-
"scopes": [ "com.atelierai.unity.copilot" ]
|
|
62
|
-
}
|
|
63
|
-
]
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Quick start
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
uco ping # health-check the bridge
|
|
71
|
-
uco call unity-tool-list # list every tool the Editor exposes
|
|
72
|
-
uco exec --method-body --return-type int --code 'return GameObject.FindObjectsOfType<Camera>().Length;'
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Configuration lives in `UserSettings/uco-config.json` (migrated automatically
|
|
76
|
-
from the legacy filename by older→newer versions of the plugin).
|
|
77
|
-
|
|
78
|
-
## Wire protocol
|
|
79
|
-
|
|
80
|
-
Plain HTTP + WebSocket with JSON envelopes (`/api/tools/...`).
|
|
81
|
-
protocol on the wire — the bridge was deliberately stripped down to REST so
|
|
82
|
-
any agent, script, or CI job can
|
|
83
|
-
|
|
84
|
-
## Attribution & license
|
|
85
|
-
|
|
86
|
-
Derived from [IvanMurzak/Unity-MCP](https://github.com/IvanMurzak/Unity-MCP)
|
|
87
|
-
(Apache-2.0). Licensed under the Apache License 2.0 — see [LICENSE](LICENSE)
|
|
88
|
-
for the dual copyright notice.
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Unity Copilot</h1>
|
|
3
|
+
<p>Drive the Unity Editor from any AI agent over plain REST + WebSocket — a single simple wire protocol on the wire.</p>
|
|
4
|
+
<p><code>com.atelierai.unity.copilot</code></p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
Unity Copilot is a Unity Editor/Runtime plugin that exposes the Editor as an
|
|
8
|
+
HTTP service: 160+ tools covering scenes, GameObjects, components, assets,
|
|
9
|
+
prefabs, scripts, packages, screenshots, console, tests, and builds — plus
|
|
10
|
+
prompts and resources. A self-hosted Node bridge connects the Editor to the
|
|
11
|
+
outside world; the **uco** CLI drives the whole surface from any agent or
|
|
12
|
+
terminal.
|
|
13
|
+
|
|
14
|
+
## Highlights
|
|
15
|
+
|
|
16
|
+
- **160+ Editor tools** — authoring, code (including Roslyn compile-and-execute
|
|
17
|
+
with confirmation gating for risky operations), diagnostics, build & tests,
|
|
18
|
+
visuals, physics
|
|
19
|
+
- **Three-surface AI skills** — `uco setup-skills` generates focused skill
|
|
20
|
+
bundles (setup / lifecycle / live-editor) for Claude Code, Codex, Cursor,
|
|
21
|
+
and other agents, refreshed from the live tool catalog
|
|
22
|
+
- **Deterministic, safe connectivity** — one port per project (hashed from the
|
|
23
|
+
project path), bearer-token auth, localhost by default, no vendor lock-in:
|
|
24
|
+
anything that can speak HTTP or run a CLI can drive the Editor
|
|
25
|
+
- **Durable calls** — async invocations return a call id queryable later
|
|
26
|
+
(`uco call get <id>`); structured error envelopes survive the whole stack
|
|
27
|
+
- **Dual-editor support** — gates run against Unity 2022.3 LTS and 6000.5
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Unity **2022.3 or newer** (tested on 2022.3 LTS and 6000.5)
|
|
32
|
+
- **Node.js 20+** for the bridge server (the plugin auto-discovers the `uco`
|
|
33
|
+
npm package — global or project-local — or set `nodeServerPath` in the
|
|
34
|
+
config to point at a specific install)
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
### Via the uco CLI (recommended)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g @atelierai/uco
|
|
42
|
+
uco install <path-to-your-unity-project>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This embeds the plugin package, stages the framework DLLs, writes the initial
|
|
46
|
+
config, and can generate agent skills and wrappers (`uco setup-skills`).
|
|
47
|
+
|
|
48
|
+
### Via OpenUPM
|
|
49
|
+
|
|
50
|
+
Add the scoped registry to `Packages/manifest.json` and reference the package:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"com.atelierai.unity.copilot": "1.0.0"
|
|
56
|
+
},
|
|
57
|
+
"scopedRegistries": [
|
|
58
|
+
{
|
|
59
|
+
"name": "package.openupm.com",
|
|
60
|
+
"url": "https://package.openupm.com",
|
|
61
|
+
"scopes": [ "com.atelierai.unity.copilot" ]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uco ping # health-check the bridge
|
|
71
|
+
uco call unity-tool-list # list every tool the Editor exposes
|
|
72
|
+
uco exec --method-body --return-type int --code 'return GameObject.FindObjectsOfType<Camera>().Length;'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Configuration lives in `UserSettings/uco-config.json` (migrated automatically
|
|
76
|
+
from the legacy filename by older→newer versions of the plugin).
|
|
77
|
+
|
|
78
|
+
## Wire protocol
|
|
79
|
+
|
|
80
|
+
Plain HTTP + WebSocket with JSON envelopes (`/api/tools/...`). The wire is plain JSON
|
|
81
|
+
protocol on the wire — the bridge was deliberately stripped down to REST so
|
|
82
|
+
any agent, script, or CI job can drive it with nothing but an HTTP client.
|
|
83
|
+
|
|
84
|
+
## Attribution & license
|
|
85
|
+
|
|
86
|
+
Derived from [IvanMurzak/Unity-MCP](https://github.com/IvanMurzak/Unity-MCP)
|
|
87
|
+
(Apache-2.0). Licensed under the Apache License 2.0 — see [LICENSE](LICENSE)
|
|
88
|
+
for the dual copyright notice.
|
package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/BufferedFileLogStorage.cs
CHANGED
|
@@ -1,207 +1,208 @@
|
|
|
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
|
-
using System;
|
|
13
|
-
using System.Collections.Generic;
|
|
14
|
-
using System.IO;
|
|
15
|
-
using System.Linq;
|
|
16
|
-
using System.Text.Json;
|
|
17
|
-
using System.Threading.Tasks;
|
|
18
|
-
using Microsoft.Extensions.Logging;
|
|
19
|
-
using UnityEngine;
|
|
20
|
-
|
|
21
|
-
namespace com.AtelierAI.Unity.Copilot
|
|
22
|
-
{
|
|
23
|
-
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
24
|
-
public class BufferedFileLogStorage : FileLogStorage
|
|
25
|
-
{
|
|
26
|
-
protected readonly int _flushEntriesThreshold;
|
|
27
|
-
protected readonly LogEntry[] _logEntriesBuffer;
|
|
28
|
-
protected int _logEntriesBufferLength;
|
|
29
|
-
|
|
30
|
-
public BufferedFileLogStorage(
|
|
31
|
-
ILogger? logger = null,
|
|
32
|
-
int flushEntriesThreshold = 100,
|
|
33
|
-
string? cacheFilePath = null,
|
|
34
|
-
string? cacheFileName = null,
|
|
35
|
-
int fileBufferSize = 4096,
|
|
36
|
-
int maxFileSizeMB = DefaultMaxFileSizeMB,
|
|
37
|
-
JsonSerializerOptions? jsonOptions = null)
|
|
38
|
-
: base(logger, cacheFilePath, cacheFileName, fileBufferSize, maxFileSizeMB, jsonOptions)
|
|
39
|
-
{
|
|
40
|
-
if (flushEntriesThreshold <= 0)
|
|
41
|
-
throw new ArgumentOutOfRangeException(nameof(flushEntriesThreshold), "Flush entries threshold must be greater than zero.");
|
|
42
|
-
|
|
43
|
-
_flushEntriesThreshold = flushEntriesThreshold;
|
|
44
|
-
_logEntriesBuffer = new LogEntry[flushEntriesThreshold];
|
|
45
|
-
_logEntriesBufferLength = 0;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
public override void Flush()
|
|
49
|
-
{
|
|
50
|
-
if (_isDisposed.Value)
|
|
51
|
-
{
|
|
52
|
-
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
53
|
-
nameof(Flush));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
lock (_fileMutex)
|
|
57
|
-
{
|
|
58
|
-
// Flush buffered entries to file
|
|
59
|
-
if (_logEntriesBufferLength > 0)
|
|
60
|
-
{
|
|
61
|
-
var entriesToFlush = new LogEntry[_logEntriesBufferLength];
|
|
62
|
-
Array.Copy(_logEntriesBuffer, entriesToFlush, _logEntriesBufferLength);
|
|
63
|
-
base.AppendInternal(entriesToFlush);
|
|
64
|
-
_logEntriesBufferLength = 0;
|
|
65
|
-
}
|
|
66
|
-
fileWriteStream?.Flush();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
public override Task FlushAsync()
|
|
70
|
-
{
|
|
71
|
-
if (_isDisposed.Value)
|
|
72
|
-
{
|
|
73
|
-
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
74
|
-
nameof(FlushAsync));
|
|
75
|
-
return Task.CompletedTask;
|
|
76
|
-
}
|
|
77
|
-
lock (_fileMutex)
|
|
78
|
-
{
|
|
79
|
-
// Flush buffered entries to file
|
|
80
|
-
if (_logEntriesBufferLength > 0)
|
|
81
|
-
{
|
|
82
|
-
var entriesToFlush = new LogEntry[_logEntriesBufferLength];
|
|
83
|
-
Array.Copy(_logEntriesBuffer, entriesToFlush, _logEntriesBufferLength);
|
|
84
|
-
base.AppendInternal(entriesToFlush);
|
|
85
|
-
_logEntriesBufferLength = 0;
|
|
86
|
-
}
|
|
87
|
-
fileWriteStream?.Flush();
|
|
88
|
-
}
|
|
89
|
-
return Task.CompletedTask;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
protected override void AppendInternal(params LogEntry[] entries)
|
|
93
|
-
{
|
|
94
|
-
if (_isDisposed.Value)
|
|
95
|
-
{
|
|
96
|
-
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
97
|
-
nameof(AppendInternal));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (_logEntriesBufferLength >= _flushEntriesThreshold)
|
|
101
|
-
{
|
|
102
|
-
base.AppendInternal(_logEntriesBuffer);
|
|
103
|
-
_logEntriesBufferLength = 0;
|
|
104
|
-
}
|
|
105
|
-
foreach (var entry in entries)
|
|
106
|
-
{
|
|
107
|
-
_logEntriesBuffer[_logEntriesBufferLength] = entry;
|
|
108
|
-
_logEntriesBufferLength++;
|
|
109
|
-
|
|
110
|
-
if (_logEntriesBufferLength >= _flushEntriesThreshold)
|
|
111
|
-
{
|
|
112
|
-
base.AppendInternal(_logEntriesBuffer);
|
|
113
|
-
_logEntriesBufferLength = 0;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/// <summary>
|
|
119
|
-
/// Closes and disposes the current file stream if open. Clears the log cache file.
|
|
120
|
-
/// </summary>
|
|
121
|
-
public override LogClearResult Clear()
|
|
122
|
-
{
|
|
123
|
-
if (_isDisposed.Value)
|
|
124
|
-
{
|
|
125
|
-
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
126
|
-
nameof(Clear));
|
|
127
|
-
return new LogClearResult
|
|
128
|
-
{
|
|
129
|
-
Ok = false,
|
|
130
|
-
Strategy = "storage-disposed",
|
|
131
|
-
Path = filePath,
|
|
132
|
-
Error = "The log storage is disposed."
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
lock (_fileMutex)
|
|
136
|
-
{
|
|
137
|
-
_logEntriesBufferLength = 0;
|
|
138
|
-
return ClearFileLocked();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
public override LogEntry[] Query(
|
|
143
|
-
int maxEntries = 100,
|
|
144
|
-
LogType? logTypeFilter = null,
|
|
145
|
-
bool includeStackTrace = false,
|
|
146
|
-
int lastMinutes = 0)
|
|
147
|
-
{
|
|
148
|
-
if (_isDisposed.Value)
|
|
149
|
-
{
|
|
150
|
-
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
151
|
-
nameof(Query));
|
|
152
|
-
return Array.Empty<LogEntry>();
|
|
153
|
-
}
|
|
154
|
-
lock (_fileMutex)
|
|
155
|
-
{
|
|
156
|
-
return QueryInternal(maxEntries, logTypeFilter, includeStackTrace, lastMinutes);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
protected override LogEntry[] QueryInternal(
|
|
161
|
-
int maxEntries = 100,
|
|
162
|
-
LogType? logTypeFilter = null,
|
|
163
|
-
bool includeStackTrace = false,
|
|
164
|
-
int lastMinutes = 0)
|
|
165
|
-
{
|
|
166
|
-
var result = new List<LogEntry>();
|
|
167
|
-
var cutoffTime = lastMinutes > 0
|
|
168
|
-
? System.DateTime.Now.AddMinutes(-lastMinutes)
|
|
169
|
-
: System.DateTime.MinValue;
|
|
170
|
-
|
|
171
|
-
// 1. Get from buffer (Newest are at the end of buffer)
|
|
172
|
-
for (int i = _logEntriesBufferLength - 1; i >= 0; i--)
|
|
173
|
-
{
|
|
174
|
-
var entry = _logEntriesBuffer[i];
|
|
175
|
-
if (logTypeFilter.HasValue && entry.LogType != logTypeFilter.Value)
|
|
176
|
-
continue;
|
|
177
|
-
|
|
178
|
-
if (lastMinutes > 0)
|
|
179
|
-
{
|
|
180
|
-
if (entry.Timestamp < cutoffTime)
|
|
181
|
-
{
|
|
182
|
-
return result.AsEnumerable().Reverse().ToArray();
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
result.Add(entry);
|
|
187
|
-
if (result.Count >= maxEntries)
|
|
188
|
-
return result.AsEnumerable().Reverse().ToArray();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// 2. Exit if we already have enough entries
|
|
192
|
-
var neededLogsCount = maxEntries - result.Count;
|
|
193
|
-
if (neededLogsCount <= 0)
|
|
194
|
-
return result.AsEnumerable().Reverse().ToArray();
|
|
195
|
-
|
|
196
|
-
result.Reverse();
|
|
197
|
-
|
|
198
|
-
// 3. Get from file
|
|
199
|
-
var fileEntries = base.QueryInternal(neededLogsCount, logTypeFilter, includeStackTrace, lastMinutes);
|
|
200
|
-
result.AddRange(fileEntries);
|
|
201
|
-
|
|
202
|
-
return result.ToArray();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
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
|
+
using System;
|
|
13
|
+
using System.Collections.Generic;
|
|
14
|
+
using System.IO;
|
|
15
|
+
using System.Linq;
|
|
16
|
+
using System.Text.Json;
|
|
17
|
+
using System.Threading.Tasks;
|
|
18
|
+
using Microsoft.Extensions.Logging;
|
|
19
|
+
using UnityEngine;
|
|
20
|
+
|
|
21
|
+
namespace com.AtelierAI.Unity.Copilot
|
|
22
|
+
{
|
|
23
|
+
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
24
|
+
public class BufferedFileLogStorage : FileLogStorage
|
|
25
|
+
{
|
|
26
|
+
protected readonly int _flushEntriesThreshold;
|
|
27
|
+
protected readonly LogEntry[] _logEntriesBuffer;
|
|
28
|
+
protected int _logEntriesBufferLength;
|
|
29
|
+
|
|
30
|
+
public BufferedFileLogStorage(
|
|
31
|
+
ILogger? logger = null,
|
|
32
|
+
int flushEntriesThreshold = 100,
|
|
33
|
+
string? cacheFilePath = null,
|
|
34
|
+
string? cacheFileName = null,
|
|
35
|
+
int fileBufferSize = 4096,
|
|
36
|
+
int maxFileSizeMB = DefaultMaxFileSizeMB,
|
|
37
|
+
JsonSerializerOptions? jsonOptions = null)
|
|
38
|
+
: base(logger, cacheFilePath, cacheFileName, fileBufferSize, maxFileSizeMB, jsonOptions)
|
|
39
|
+
{
|
|
40
|
+
if (flushEntriesThreshold <= 0)
|
|
41
|
+
throw new ArgumentOutOfRangeException(nameof(flushEntriesThreshold), "Flush entries threshold must be greater than zero.");
|
|
42
|
+
|
|
43
|
+
_flushEntriesThreshold = flushEntriesThreshold;
|
|
44
|
+
_logEntriesBuffer = new LogEntry[flushEntriesThreshold];
|
|
45
|
+
_logEntriesBufferLength = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public override void Flush()
|
|
49
|
+
{
|
|
50
|
+
if (_isDisposed.Value)
|
|
51
|
+
{
|
|
52
|
+
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
53
|
+
nameof(Flush));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
lock (_fileMutex)
|
|
57
|
+
{
|
|
58
|
+
// Flush buffered entries to file
|
|
59
|
+
if (_logEntriesBufferLength > 0)
|
|
60
|
+
{
|
|
61
|
+
var entriesToFlush = new LogEntry[_logEntriesBufferLength];
|
|
62
|
+
Array.Copy(_logEntriesBuffer, entriesToFlush, _logEntriesBufferLength);
|
|
63
|
+
base.AppendInternal(entriesToFlush);
|
|
64
|
+
_logEntriesBufferLength = 0;
|
|
65
|
+
}
|
|
66
|
+
fileWriteStream?.Flush();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
public override Task FlushAsync()
|
|
70
|
+
{
|
|
71
|
+
if (_isDisposed.Value)
|
|
72
|
+
{
|
|
73
|
+
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
74
|
+
nameof(FlushAsync));
|
|
75
|
+
return Task.CompletedTask;
|
|
76
|
+
}
|
|
77
|
+
lock (_fileMutex)
|
|
78
|
+
{
|
|
79
|
+
// Flush buffered entries to file
|
|
80
|
+
if (_logEntriesBufferLength > 0)
|
|
81
|
+
{
|
|
82
|
+
var entriesToFlush = new LogEntry[_logEntriesBufferLength];
|
|
83
|
+
Array.Copy(_logEntriesBuffer, entriesToFlush, _logEntriesBufferLength);
|
|
84
|
+
base.AppendInternal(entriesToFlush);
|
|
85
|
+
_logEntriesBufferLength = 0;
|
|
86
|
+
}
|
|
87
|
+
fileWriteStream?.Flush();
|
|
88
|
+
}
|
|
89
|
+
return Task.CompletedTask;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected override void AppendInternal(params LogEntry[] entries)
|
|
93
|
+
{
|
|
94
|
+
if (_isDisposed.Value)
|
|
95
|
+
{
|
|
96
|
+
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
97
|
+
nameof(AppendInternal));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (_logEntriesBufferLength >= _flushEntriesThreshold)
|
|
101
|
+
{
|
|
102
|
+
base.AppendInternal(_logEntriesBuffer);
|
|
103
|
+
_logEntriesBufferLength = 0;
|
|
104
|
+
}
|
|
105
|
+
foreach (var entry in entries)
|
|
106
|
+
{
|
|
107
|
+
_logEntriesBuffer[_logEntriesBufferLength] = entry;
|
|
108
|
+
_logEntriesBufferLength++;
|
|
109
|
+
|
|
110
|
+
if (_logEntriesBufferLength >= _flushEntriesThreshold)
|
|
111
|
+
{
|
|
112
|
+
base.AppendInternal(_logEntriesBuffer);
|
|
113
|
+
_logEntriesBufferLength = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// <summary>
|
|
119
|
+
/// Closes and disposes the current file stream if open. Clears the log cache file.
|
|
120
|
+
/// </summary>
|
|
121
|
+
public override LogClearResult Clear()
|
|
122
|
+
{
|
|
123
|
+
if (_isDisposed.Value)
|
|
124
|
+
{
|
|
125
|
+
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
126
|
+
nameof(Clear));
|
|
127
|
+
return new LogClearResult
|
|
128
|
+
{
|
|
129
|
+
Ok = false,
|
|
130
|
+
Strategy = "storage-disposed",
|
|
131
|
+
Path = filePath,
|
|
132
|
+
Error = "The log storage is disposed."
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
lock (_fileMutex)
|
|
136
|
+
{
|
|
137
|
+
_logEntriesBufferLength = 0;
|
|
138
|
+
return ClearFileLocked();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public override LogEntry[] Query(
|
|
143
|
+
int maxEntries = 100,
|
|
144
|
+
LogType? logTypeFilter = null,
|
|
145
|
+
bool includeStackTrace = false,
|
|
146
|
+
int lastMinutes = 0)
|
|
147
|
+
{
|
|
148
|
+
if (_isDisposed.Value)
|
|
149
|
+
{
|
|
150
|
+
_logger.LogWarning("{method} called but already disposed, ignored.",
|
|
151
|
+
nameof(Query));
|
|
152
|
+
return Array.Empty<LogEntry>();
|
|
153
|
+
}
|
|
154
|
+
lock (_fileMutex)
|
|
155
|
+
{
|
|
156
|
+
return QueryInternal(maxEntries, logTypeFilter, includeStackTrace, lastMinutes);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
protected override LogEntry[] QueryInternal(
|
|
161
|
+
int maxEntries = 100,
|
|
162
|
+
LogType? logTypeFilter = null,
|
|
163
|
+
bool includeStackTrace = false,
|
|
164
|
+
int lastMinutes = 0)
|
|
165
|
+
{
|
|
166
|
+
var result = new List<LogEntry>();
|
|
167
|
+
var cutoffTime = lastMinutes > 0
|
|
168
|
+
? System.DateTime.Now.AddMinutes(-lastMinutes)
|
|
169
|
+
: System.DateTime.MinValue;
|
|
170
|
+
|
|
171
|
+
// 1. Get from buffer (Newest are at the end of buffer)
|
|
172
|
+
for (int i = _logEntriesBufferLength - 1; i >= 0; i--)
|
|
173
|
+
{
|
|
174
|
+
var entry = _logEntriesBuffer[i];
|
|
175
|
+
if (logTypeFilter.HasValue && entry.LogType != logTypeFilter.Value)
|
|
176
|
+
continue;
|
|
177
|
+
|
|
178
|
+
if (lastMinutes > 0)
|
|
179
|
+
{
|
|
180
|
+
if (entry.Timestamp < cutoffTime)
|
|
181
|
+
{
|
|
182
|
+
return result.AsEnumerable().Reverse().ToArray();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
result.Add(entry);
|
|
187
|
+
if (result.Count >= maxEntries)
|
|
188
|
+
return result.AsEnumerable().Reverse().ToArray();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 2. Exit if we already have enough entries
|
|
192
|
+
var neededLogsCount = maxEntries - result.Count;
|
|
193
|
+
if (neededLogsCount <= 0)
|
|
194
|
+
return result.AsEnumerable().Reverse().ToArray();
|
|
195
|
+
|
|
196
|
+
result.Reverse();
|
|
197
|
+
|
|
198
|
+
// 3. Get from file
|
|
199
|
+
var fileEntries = base.QueryInternal(neededLogsCount, logTypeFilter, includeStackTrace, lastMinutes);
|
|
200
|
+
result.AddRange(fileEntries);
|
|
201
|
+
|
|
202
|
+
return result.ToArray();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// No finalizer: Dispose is plain managed cleanup; running arbitrary code on
|
|
206
|
+
// the finalizer thread during domain unload is never safe.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -542,6 +542,8 @@ namespace com.AtelierAI.Unity.Copilot
|
|
|
542
542
|
GC.SuppressFinalize(this);
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
-
|
|
545
|
+
// No finalizer: Dispose performs file I/O / Unity static-event unsubscription,
|
|
546
|
+
// which must never run on the finalizer thread during domain unload
|
|
547
|
+
// (same crash class as uco-domain-reload-crash-20260919).
|
|
546
548
|
}
|
|
547
549
|
}
|