@mflrevan/ucp 0.5.1 → 0.6.0
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/README.md +1 -1
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs +3 -0
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +16 -3
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +172 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +88 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs +60 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +56 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +325 -13
- package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +207 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +14 -35
- package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +6 -6
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +2 -34
- package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs +151 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +304 -9
- package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs +322 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +249 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +415 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +135 -7
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +252 -0
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs.meta +2 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
using System;
|
|
2
2
|
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
3
4
|
using System.Linq;
|
|
5
|
+
using System.Reflection;
|
|
4
6
|
using System.Text.RegularExpressions;
|
|
7
|
+
using UnityEditor;
|
|
5
8
|
using UnityEngine;
|
|
6
9
|
|
|
7
10
|
namespace UCP.Bridge
|
|
@@ -14,7 +17,10 @@ namespace UCP.Bridge
|
|
|
14
17
|
|
|
15
18
|
private static readonly object s_historyLock = new object();
|
|
16
19
|
private static readonly List<LogRecord> s_history = new List<LogRecord>();
|
|
20
|
+
private static Func<List<ConsoleBackfillEntry>> s_consoleBackfillProvider = CaptureRecentConsoleEntries;
|
|
17
21
|
private static long s_nextId = 1;
|
|
22
|
+
private static StreamWriter s_captureWriter;
|
|
23
|
+
private static string s_capturePath;
|
|
18
24
|
|
|
19
25
|
public static void Register(CommandRouter router)
|
|
20
26
|
{
|
|
@@ -24,6 +30,8 @@ namespace UCP.Bridge
|
|
|
24
30
|
router.Register("logs/search", HandleSearch);
|
|
25
31
|
router.Register("logs/get", HandleGet);
|
|
26
32
|
router.Register("logs/status", HandleStatus);
|
|
33
|
+
router.Register("logs/capture/start", HandleCaptureStart);
|
|
34
|
+
router.Register("logs/capture/stop", HandleCaptureStop);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
public static Dictionary<string, object> RecordLog(string message, string stackTrace, LogType type)
|
|
@@ -37,6 +45,7 @@ namespace UCP.Bridge
|
|
|
37
45
|
{
|
|
38
46
|
s_history.Clear();
|
|
39
47
|
s_nextId = 1;
|
|
48
|
+
s_consoleBackfillProvider = CaptureRecentConsoleEntries;
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
|
|
@@ -45,8 +54,64 @@ namespace UCP.Bridge
|
|
|
45
54
|
return RecordLog(level, message, stackTrace);
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
public static void SetConsoleBackfillProviderForTests(Func<List<ConsoleBackfillEntry>> provider)
|
|
58
|
+
{
|
|
59
|
+
lock (s_historyLock)
|
|
60
|
+
{
|
|
61
|
+
s_consoleBackfillProvider = provider ?? CaptureRecentConsoleEntries;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public static void SeedHistoryFromConsole()
|
|
66
|
+
{
|
|
67
|
+
lock (s_historyLock)
|
|
68
|
+
{
|
|
69
|
+
SeedHistoryFromConsoleLocked();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public static Dictionary<string, object> StartFileCapture(string path)
|
|
74
|
+
{
|
|
75
|
+
lock (s_historyLock)
|
|
76
|
+
{
|
|
77
|
+
StopFileCaptureLocked();
|
|
78
|
+
|
|
79
|
+
var resolvedPath = ResolveCapturePath(path);
|
|
80
|
+
var directory = Path.GetDirectoryName(resolvedPath);
|
|
81
|
+
if (!string.IsNullOrEmpty(directory))
|
|
82
|
+
Directory.CreateDirectory(directory);
|
|
83
|
+
|
|
84
|
+
s_captureWriter = new StreamWriter(resolvedPath, false);
|
|
85
|
+
s_captureWriter.AutoFlush = true;
|
|
86
|
+
s_capturePath = resolvedPath;
|
|
87
|
+
s_captureWriter.WriteLine($"# UCP play-mode log capture started {DateTime.UtcNow:o}");
|
|
88
|
+
|
|
89
|
+
return new Dictionary<string, object>
|
|
90
|
+
{
|
|
91
|
+
["status"] = "ok",
|
|
92
|
+
["path"] = resolvedPath
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public static Dictionary<string, object> StopFileCapture()
|
|
98
|
+
{
|
|
99
|
+
lock (s_historyLock)
|
|
100
|
+
{
|
|
101
|
+
var path = s_capturePath;
|
|
102
|
+
StopFileCaptureLocked();
|
|
103
|
+
return new Dictionary<string, object>
|
|
104
|
+
{
|
|
105
|
+
["status"] = "ok",
|
|
106
|
+
["path"] = path ?? string.Empty,
|
|
107
|
+
["active"] = false
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
48
112
|
public static long GetLatestId()
|
|
49
113
|
{
|
|
114
|
+
SeedHistoryFromConsole();
|
|
50
115
|
lock (s_historyLock)
|
|
51
116
|
{
|
|
52
117
|
return s_history.Count == 0 ? 0 : s_history[s_history.Count - 1].Id;
|
|
@@ -55,6 +120,7 @@ namespace UCP.Bridge
|
|
|
55
120
|
|
|
56
121
|
public static Dictionary<string, object> BuildStatusSummary(long afterId = 0)
|
|
57
122
|
{
|
|
123
|
+
SeedHistoryFromConsole();
|
|
58
124
|
lock (s_historyLock)
|
|
59
125
|
{
|
|
60
126
|
var ordered = s_history
|
|
@@ -86,6 +152,7 @@ namespace UCP.Bridge
|
|
|
86
152
|
|
|
87
153
|
long id = Convert.ToInt64(idObj);
|
|
88
154
|
|
|
155
|
+
SeedHistoryFromConsole();
|
|
89
156
|
lock (s_historyLock)
|
|
90
157
|
{
|
|
91
158
|
var entry = s_history.FirstOrDefault(record => record.Id == id);
|
|
@@ -106,28 +173,74 @@ namespace UCP.Bridge
|
|
|
106
173
|
return BuildStatusSummary(afterId);
|
|
107
174
|
}
|
|
108
175
|
|
|
176
|
+
private static object HandleCaptureStart(string paramsJson)
|
|
177
|
+
{
|
|
178
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
179
|
+
if (p == null || !p.TryGetValue("path", out var pathObj) || pathObj == null)
|
|
180
|
+
throw new ArgumentException("Missing 'path' parameter");
|
|
181
|
+
|
|
182
|
+
return StartFileCapture(pathObj.ToString());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static object HandleCaptureStop(string paramsJson)
|
|
186
|
+
{
|
|
187
|
+
return StopFileCapture();
|
|
188
|
+
}
|
|
189
|
+
|
|
109
190
|
private static Dictionary<string, object> RecordLog(string level, string message, string stackTrace)
|
|
110
191
|
{
|
|
111
192
|
lock (s_historyLock)
|
|
112
193
|
{
|
|
113
|
-
var entry =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
121
|
-
entry.Timestamp = entry.TimestampUtc.ToString("o");
|
|
122
|
-
|
|
123
|
-
s_history.Add(entry);
|
|
124
|
-
if (s_history.Count > MaxHistoryEntries)
|
|
125
|
-
s_history.RemoveAt(0);
|
|
194
|
+
var entry = AppendRecordLocked(
|
|
195
|
+
NormalizeLevel(level),
|
|
196
|
+
message ?? string.Empty,
|
|
197
|
+
stackTrace ?? string.Empty,
|
|
198
|
+
DateTime.UtcNow
|
|
199
|
+
);
|
|
200
|
+
WriteCaptureLineLocked(entry);
|
|
126
201
|
|
|
127
202
|
return SerializeFull(entry);
|
|
128
203
|
}
|
|
129
204
|
}
|
|
130
205
|
|
|
206
|
+
private static void WriteCaptureLineLocked(LogRecord entry)
|
|
207
|
+
{
|
|
208
|
+
if (s_captureWriter == null)
|
|
209
|
+
return;
|
|
210
|
+
|
|
211
|
+
var message = (entry.Message ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n");
|
|
212
|
+
s_captureWriter.WriteLine($"{entry.Timestamp} [{entry.Level}] {message}");
|
|
213
|
+
if (!string.IsNullOrEmpty(entry.StackTrace))
|
|
214
|
+
{
|
|
215
|
+
var stack = entry.StackTrace.Replace("\r", "\\r").Replace("\n", "\\n");
|
|
216
|
+
s_captureWriter.WriteLine($"{entry.Timestamp} [stack] {stack}");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private static void StopFileCaptureLocked()
|
|
221
|
+
{
|
|
222
|
+
if (s_captureWriter != null)
|
|
223
|
+
{
|
|
224
|
+
s_captureWriter.WriteLine($"# UCP log capture stopped {DateTime.UtcNow:o}");
|
|
225
|
+
s_captureWriter.Dispose();
|
|
226
|
+
s_captureWriter = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
s_capturePath = null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private static string ResolveCapturePath(string path)
|
|
233
|
+
{
|
|
234
|
+
if (string.IsNullOrEmpty(path))
|
|
235
|
+
throw new ArgumentException("Log capture path cannot be empty");
|
|
236
|
+
|
|
237
|
+
if (Path.IsPathRooted(path))
|
|
238
|
+
return Path.GetFullPath(path);
|
|
239
|
+
|
|
240
|
+
var projectRoot = Path.GetDirectoryName(Application.dataPath);
|
|
241
|
+
return Path.GetFullPath(Path.Combine(projectRoot, path));
|
|
242
|
+
}
|
|
243
|
+
|
|
131
244
|
private static LogQuery ParseQuery(string paramsJson, bool includePattern)
|
|
132
245
|
{
|
|
133
246
|
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
@@ -137,6 +250,8 @@ namespace UCP.Bridge
|
|
|
137
250
|
{
|
|
138
251
|
if (p.TryGetValue("level", out var levelObj) && levelObj != null)
|
|
139
252
|
query.Level = NormalizeLevel(levelObj.ToString());
|
|
253
|
+
if (p.TryGetValue("channel", out var channelObj) && channelObj != null)
|
|
254
|
+
query.Channel = channelObj.ToString();
|
|
140
255
|
if (p.TryGetValue("count", out var countObj) && countObj != null)
|
|
141
256
|
query.Count = Math.Max(1, Convert.ToInt32(countObj));
|
|
142
257
|
if (p.TryGetValue("beforeId", out var beforeObj) && beforeObj != null)
|
|
@@ -166,6 +281,7 @@ namespace UCP.Bridge
|
|
|
166
281
|
|
|
167
282
|
private static LogQueryResult QueryHistory(LogQuery query)
|
|
168
283
|
{
|
|
284
|
+
SeedHistoryFromConsole();
|
|
169
285
|
lock (s_historyLock)
|
|
170
286
|
{
|
|
171
287
|
IEnumerable<LogRecord> candidates = s_history;
|
|
@@ -176,6 +292,10 @@ namespace UCP.Bridge
|
|
|
176
292
|
candidates = candidates.Where(entry => entry.Id > query.AfterId.Value);
|
|
177
293
|
if (!string.IsNullOrEmpty(query.Level))
|
|
178
294
|
candidates = candidates.Where(entry => PassesLevel(entry.Level, query.Level));
|
|
295
|
+
if (!string.IsNullOrEmpty(query.Channel))
|
|
296
|
+
candidates = candidates.Where(entry =>
|
|
297
|
+
(entry.Message ?? string.Empty).IndexOf(query.Channel, StringComparison.OrdinalIgnoreCase) >= 0
|
|
298
|
+
|| (entry.StackTrace ?? string.Empty).IndexOf(query.Channel, StringComparison.OrdinalIgnoreCase) >= 0);
|
|
179
299
|
|
|
180
300
|
if (query.Regex != null)
|
|
181
301
|
{
|
|
@@ -361,6 +481,184 @@ namespace UCP.Bridge
|
|
|
361
481
|
}
|
|
362
482
|
}
|
|
363
483
|
|
|
484
|
+
private static void SeedHistoryFromConsoleLocked()
|
|
485
|
+
{
|
|
486
|
+
if (s_history.Count > 0)
|
|
487
|
+
return;
|
|
488
|
+
|
|
489
|
+
List<ConsoleBackfillEntry> consoleEntries;
|
|
490
|
+
try
|
|
491
|
+
{
|
|
492
|
+
consoleEntries = s_consoleBackfillProvider != null
|
|
493
|
+
? s_consoleBackfillProvider.Invoke()
|
|
494
|
+
: new List<ConsoleBackfillEntry>();
|
|
495
|
+
}
|
|
496
|
+
catch
|
|
497
|
+
{
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (consoleEntries == null || consoleEntries.Count == 0)
|
|
502
|
+
return;
|
|
503
|
+
|
|
504
|
+
var baseTimestamp = DateTime.UtcNow;
|
|
505
|
+
for (var index = 0; index < consoleEntries.Count; index++)
|
|
506
|
+
{
|
|
507
|
+
var entry = consoleEntries[index];
|
|
508
|
+
if (entry == null || string.IsNullOrEmpty(entry.Message) || entry.Message.StartsWith("[UCP]", StringComparison.Ordinal))
|
|
509
|
+
continue;
|
|
510
|
+
|
|
511
|
+
AppendRecordLocked(
|
|
512
|
+
entry.Level,
|
|
513
|
+
entry.Message,
|
|
514
|
+
entry.StackTrace,
|
|
515
|
+
baseTimestamp.AddMilliseconds(index)
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private static LogRecord AppendRecordLocked(string level, string message, string stackTrace, DateTime timestampUtc)
|
|
521
|
+
{
|
|
522
|
+
var entry = new LogRecord
|
|
523
|
+
{
|
|
524
|
+
Id = s_nextId++,
|
|
525
|
+
Level = NormalizeLevel(level),
|
|
526
|
+
Message = message ?? string.Empty,
|
|
527
|
+
StackTrace = stackTrace ?? string.Empty,
|
|
528
|
+
TimestampUtc = timestampUtc
|
|
529
|
+
};
|
|
530
|
+
entry.Timestamp = entry.TimestampUtc.ToString("o");
|
|
531
|
+
|
|
532
|
+
s_history.Add(entry);
|
|
533
|
+
if (s_history.Count > MaxHistoryEntries)
|
|
534
|
+
s_history.RemoveAt(0);
|
|
535
|
+
|
|
536
|
+
return entry;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private static List<ConsoleBackfillEntry> CaptureRecentConsoleEntries()
|
|
540
|
+
{
|
|
541
|
+
try
|
|
542
|
+
{
|
|
543
|
+
var assembly = typeof(Editor).Assembly;
|
|
544
|
+
var logEntriesType = assembly.GetType("UnityEditor.LogEntries");
|
|
545
|
+
var logEntryType = assembly.GetType("UnityEditor.LogEntry");
|
|
546
|
+
if (logEntriesType == null || logEntryType == null)
|
|
547
|
+
return new List<ConsoleBackfillEntry>();
|
|
548
|
+
|
|
549
|
+
var getCount = logEntriesType.GetMethod("GetCount", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
|
|
550
|
+
var getEntry = logEntriesType
|
|
551
|
+
.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
|
|
552
|
+
.FirstOrDefault(method => method.Name == "GetEntryInternal");
|
|
553
|
+
var messageField = logEntryType.GetField("message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
|
554
|
+
var modeField = logEntryType.GetField("mode", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
|
555
|
+
if (getCount == null || getEntry == null || messageField == null || modeField == null)
|
|
556
|
+
return new List<ConsoleBackfillEntry>();
|
|
557
|
+
|
|
558
|
+
var count = Convert.ToInt32(getCount.Invoke(null, null));
|
|
559
|
+
if (count <= 0)
|
|
560
|
+
return new List<ConsoleBackfillEntry>();
|
|
561
|
+
|
|
562
|
+
var captured = new List<ConsoleBackfillEntry>();
|
|
563
|
+
var start = Math.Max(0, count - MaxHistoryEntries);
|
|
564
|
+
for (var index = start; index < count; index++)
|
|
565
|
+
{
|
|
566
|
+
var args = new object[] { index, Activator.CreateInstance(logEntryType) };
|
|
567
|
+
if (!Convert.ToBoolean(getEntry.Invoke(null, args)))
|
|
568
|
+
continue;
|
|
569
|
+
|
|
570
|
+
var entry = CreateConsoleBackfillEntry(
|
|
571
|
+
messageField.GetValue(args[1]) as string,
|
|
572
|
+
Convert.ToInt32(modeField.GetValue(args[1]))
|
|
573
|
+
);
|
|
574
|
+
if (entry != null)
|
|
575
|
+
captured.Add(entry);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return captured;
|
|
579
|
+
}
|
|
580
|
+
catch
|
|
581
|
+
{
|
|
582
|
+
return new List<ConsoleBackfillEntry>();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private static ConsoleBackfillEntry CreateConsoleBackfillEntry(string rawMessage, int mode)
|
|
587
|
+
{
|
|
588
|
+
var normalized = (rawMessage ?? string.Empty).Replace("\r\n", "\n").TrimEnd();
|
|
589
|
+
if (string.IsNullOrEmpty(normalized) || normalized.StartsWith("[UCP]", StringComparison.Ordinal))
|
|
590
|
+
return null;
|
|
591
|
+
|
|
592
|
+
var split = SplitConsoleMessage(normalized);
|
|
593
|
+
return new ConsoleBackfillEntry
|
|
594
|
+
{
|
|
595
|
+
Level = InferConsoleLevel(mode, split.Message, split.StackTrace),
|
|
596
|
+
Message = split.Message,
|
|
597
|
+
StackTrace = split.StackTrace
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private static SplitLogMessage SplitConsoleMessage(string rawMessage)
|
|
602
|
+
{
|
|
603
|
+
var firstNewline = rawMessage.IndexOf('\n');
|
|
604
|
+
if (firstNewline < 0)
|
|
605
|
+
{
|
|
606
|
+
return new SplitLogMessage
|
|
607
|
+
{
|
|
608
|
+
Message = rawMessage,
|
|
609
|
+
StackTrace = string.Empty
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
var firstLine = rawMessage.Substring(0, firstNewline).TrimEnd();
|
|
614
|
+
var remainder = rawMessage.Substring(firstNewline + 1).Trim();
|
|
615
|
+
if (LooksLikeStackTrace(remainder))
|
|
616
|
+
{
|
|
617
|
+
return new SplitLogMessage
|
|
618
|
+
{
|
|
619
|
+
Message = firstLine,
|
|
620
|
+
StackTrace = remainder
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return new SplitLogMessage
|
|
625
|
+
{
|
|
626
|
+
Message = rawMessage,
|
|
627
|
+
StackTrace = string.Empty
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private static bool LooksLikeStackTrace(string value)
|
|
632
|
+
{
|
|
633
|
+
if (string.IsNullOrEmpty(value))
|
|
634
|
+
return false;
|
|
635
|
+
|
|
636
|
+
return value.StartsWith("UnityEngine.", StringComparison.Ordinal)
|
|
637
|
+
|| value.StartsWith("UnityEditor.", StringComparison.Ordinal)
|
|
638
|
+
|| value.StartsWith("System.", StringComparison.Ordinal)
|
|
639
|
+
|| value.StartsWith("---", StringComparison.Ordinal)
|
|
640
|
+
|| value.Contains(" (at ")
|
|
641
|
+
|| value.IndexOf(":Log", StringComparison.Ordinal) >= 0
|
|
642
|
+
|| value.IndexOf(":LogWarning", StringComparison.Ordinal) >= 0
|
|
643
|
+
|| value.IndexOf(":LogError", StringComparison.Ordinal) >= 0
|
|
644
|
+
|| value.IndexOf(":LogException", StringComparison.Ordinal) >= 0;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private static string InferConsoleLevel(int mode, string message, string stackTrace)
|
|
648
|
+
{
|
|
649
|
+
var combined = (message ?? string.Empty) + "\n" + (stackTrace ?? string.Empty);
|
|
650
|
+
if ((mode & 0x100) != 0 || combined.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
651
|
+
return combined.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0 ? "exception" : "error";
|
|
652
|
+
if ((mode & 0x200) != 0)
|
|
653
|
+
return "warning";
|
|
654
|
+
if (Regex.IsMatch(combined, @"\bwarning\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
|
|
655
|
+
return "warning";
|
|
656
|
+
if (Regex.IsMatch(combined, @"\berror\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
|
|
657
|
+
return combined.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0 ? "exception" : "error";
|
|
658
|
+
|
|
659
|
+
return "info";
|
|
660
|
+
}
|
|
661
|
+
|
|
364
662
|
private static string Preview(string value, int maxChars)
|
|
365
663
|
{
|
|
366
664
|
if (string.IsNullOrEmpty(value) || value.Length <= maxChars)
|
|
@@ -417,9 +715,23 @@ namespace UCP.Bridge
|
|
|
417
715
|
public DateTime TimestampUtc;
|
|
418
716
|
}
|
|
419
717
|
|
|
718
|
+
public sealed class ConsoleBackfillEntry
|
|
719
|
+
{
|
|
720
|
+
public string Level;
|
|
721
|
+
public string Message;
|
|
722
|
+
public string StackTrace;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private sealed class SplitLogMessage
|
|
726
|
+
{
|
|
727
|
+
public string Message;
|
|
728
|
+
public string StackTrace;
|
|
729
|
+
}
|
|
730
|
+
|
|
420
731
|
private sealed class LogQuery
|
|
421
732
|
{
|
|
422
733
|
public string Level;
|
|
734
|
+
public string Channel;
|
|
423
735
|
public string Pattern;
|
|
424
736
|
public Regex Regex;
|
|
425
737
|
public int Count;
|
|
@@ -50,7 +50,7 @@ namespace UCP.Bridge
|
|
|
50
50
|
["path"] = path,
|
|
51
51
|
["name"] = material.name,
|
|
52
52
|
["shader"] = shader.name,
|
|
53
|
-
["instanceId"] = material.
|
|
53
|
+
["instanceId"] = material.GetId()
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -278,7 +278,7 @@ namespace UCP.Bridge
|
|
|
278
278
|
{
|
|
279
279
|
["name"] = tex.name,
|
|
280
280
|
["path"] = texPath,
|
|
281
|
-
["instanceId"] = tex.
|
|
281
|
+
["instanceId"] = tex.GetId()
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
284
|
return null;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using UnityEngine;
|
|
4
|
+
using UnityEngine.SceneManagement;
|
|
5
|
+
|
|
6
|
+
namespace UCP.Bridge
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Shared GameObject resolution for the spatial/visual controllers.
|
|
10
|
+
///
|
|
11
|
+
/// A target may be addressed three ways, tried in priority order:
|
|
12
|
+
/// 1. instanceId (int) — canonical, survives nothing but a domain reload; preferred.
|
|
13
|
+
/// 2. path (string) — hierarchy path "Root/Child/Leaf" (leading '/' optional),
|
|
14
|
+
/// resolved across all loaded scenes; survives reloads.
|
|
15
|
+
/// 3. name (string) — first GameObject whose name matches; ambiguous under
|
|
16
|
+
/// duplicates, so it is the last resort.
|
|
17
|
+
///
|
|
18
|
+
/// instanceId stays the deterministic handle. path/name are convenience fallbacks so an
|
|
19
|
+
/// agent does not have to re-snapshot after every reload just to re-acquire an id.
|
|
20
|
+
/// </summary>
|
|
21
|
+
internal static class ObjectLocator
|
|
22
|
+
{
|
|
23
|
+
/// <summary>
|
|
24
|
+
/// Resolve a GameObject from a params dictionary using whichever of
|
|
25
|
+
/// instanceId / id / path / name is present (in that priority order).
|
|
26
|
+
/// Throws ArgumentException if none are present or nothing resolves.
|
|
27
|
+
/// </summary>
|
|
28
|
+
internal static GameObject Resolve(Dictionary<string, object> p)
|
|
29
|
+
{
|
|
30
|
+
if (p == null)
|
|
31
|
+
throw new ArgumentException("Missing target: provide 'instanceId', 'path', or 'name'");
|
|
32
|
+
|
|
33
|
+
if ((p.TryGetValue("instanceId", out var idObj) || p.TryGetValue("id", out idObj)) && idObj != null)
|
|
34
|
+
{
|
|
35
|
+
var id = Convert.ToInt32(idObj);
|
|
36
|
+
var byId = FindByInstanceId(id);
|
|
37
|
+
if (byId != null)
|
|
38
|
+
return byId;
|
|
39
|
+
throw new ArgumentException($"GameObject not found for instanceId {id}");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (p.TryGetValue("path", out var pathObj) && pathObj != null)
|
|
43
|
+
{
|
|
44
|
+
var path = pathObj.ToString();
|
|
45
|
+
var byPath = FindByPath(path);
|
|
46
|
+
if (byPath != null)
|
|
47
|
+
return byPath;
|
|
48
|
+
throw new ArgumentException($"GameObject not found for path '{path}'");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (p.TryGetValue("name", out var nameObj) && nameObj != null)
|
|
52
|
+
{
|
|
53
|
+
var name = nameObj.ToString();
|
|
54
|
+
var byName = FindByName(name);
|
|
55
|
+
if (byName != null)
|
|
56
|
+
return byName;
|
|
57
|
+
throw new ArgumentException($"GameObject not found for name '{name}'");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new ArgumentException("Missing target: provide 'instanceId', 'path', or 'name'");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
internal static GameObject FindByInstanceId(int instanceId)
|
|
64
|
+
{
|
|
65
|
+
var direct = UnityObjectCompat.ResolveByInstanceId<GameObject>(instanceId);
|
|
66
|
+
if (direct != null)
|
|
67
|
+
return direct;
|
|
68
|
+
|
|
69
|
+
for (var i = 0; i < SceneManager.sceneCount; i++)
|
|
70
|
+
{
|
|
71
|
+
var scene = SceneManager.GetSceneAt(i);
|
|
72
|
+
if (!scene.isLoaded)
|
|
73
|
+
continue;
|
|
74
|
+
foreach (var root in scene.GetRootGameObjects())
|
|
75
|
+
{
|
|
76
|
+
var found = FindInHierarchyById(root, instanceId);
|
|
77
|
+
if (found != null)
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static GameObject FindInHierarchyById(GameObject go, int instanceId)
|
|
86
|
+
{
|
|
87
|
+
if (go.GetId() == instanceId)
|
|
88
|
+
return go;
|
|
89
|
+
foreach (Transform child in go.transform)
|
|
90
|
+
{
|
|
91
|
+
var found = FindInHierarchyById(child.gameObject, instanceId);
|
|
92
|
+
if (found != null)
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
internal static GameObject FindByPath(string path)
|
|
99
|
+
{
|
|
100
|
+
if (string.IsNullOrEmpty(path))
|
|
101
|
+
return null;
|
|
102
|
+
|
|
103
|
+
var trimmed = path.Trim('/');
|
|
104
|
+
var segments = trimmed.Split('/');
|
|
105
|
+
if (segments.Length == 0)
|
|
106
|
+
return null;
|
|
107
|
+
|
|
108
|
+
for (var i = 0; i < SceneManager.sceneCount; i++)
|
|
109
|
+
{
|
|
110
|
+
var scene = SceneManager.GetSceneAt(i);
|
|
111
|
+
if (!scene.isLoaded)
|
|
112
|
+
continue;
|
|
113
|
+
foreach (var root in scene.GetRootGameObjects())
|
|
114
|
+
{
|
|
115
|
+
if (root.name != segments[0])
|
|
116
|
+
continue;
|
|
117
|
+
var resolved = WalkPath(root.transform, segments, 1);
|
|
118
|
+
if (resolved != null)
|
|
119
|
+
return resolved;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private static GameObject WalkPath(Transform current, string[] segments, int index)
|
|
127
|
+
{
|
|
128
|
+
if (index >= segments.Length)
|
|
129
|
+
return current.gameObject;
|
|
130
|
+
|
|
131
|
+
foreach (Transform child in current)
|
|
132
|
+
{
|
|
133
|
+
if (child.name == segments[index])
|
|
134
|
+
{
|
|
135
|
+
var resolved = WalkPath(child, segments, index + 1);
|
|
136
|
+
if (resolved != null)
|
|
137
|
+
return resolved;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
internal static GameObject FindByName(string name)
|
|
145
|
+
{
|
|
146
|
+
if (string.IsNullOrEmpty(name))
|
|
147
|
+
return null;
|
|
148
|
+
|
|
149
|
+
for (var i = 0; i < SceneManager.sceneCount; i++)
|
|
150
|
+
{
|
|
151
|
+
var scene = SceneManager.GetSceneAt(i);
|
|
152
|
+
if (!scene.isLoaded)
|
|
153
|
+
continue;
|
|
154
|
+
foreach (var root in scene.GetRootGameObjects())
|
|
155
|
+
{
|
|
156
|
+
var found = FindInHierarchyByName(root, name);
|
|
157
|
+
if (found != null)
|
|
158
|
+
return found;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private static GameObject FindInHierarchyByName(GameObject go, string name)
|
|
166
|
+
{
|
|
167
|
+
if (go.name == name)
|
|
168
|
+
return go;
|
|
169
|
+
foreach (Transform child in go.transform)
|
|
170
|
+
{
|
|
171
|
+
var found = FindInHierarchyByName(child.gameObject, name);
|
|
172
|
+
if (found != null)
|
|
173
|
+
return found;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// <summary>Compute a world-space AABB encapsulating the object's renderers and colliders.</summary>
|
|
179
|
+
internal static bool TryComputeWorldBounds(GameObject target, bool includeChildren, out Bounds bounds)
|
|
180
|
+
{
|
|
181
|
+
var hasBounds = false;
|
|
182
|
+
bounds = new Bounds(target.transform.position, Vector3.zero);
|
|
183
|
+
|
|
184
|
+
var renderers = includeChildren
|
|
185
|
+
? target.GetComponentsInChildren<Renderer>()
|
|
186
|
+
: target.GetComponents<Renderer>();
|
|
187
|
+
foreach (var renderer in renderers)
|
|
188
|
+
{
|
|
189
|
+
if (!hasBounds) { bounds = renderer.bounds; hasBounds = true; }
|
|
190
|
+
else bounds.Encapsulate(renderer.bounds);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var colliders = includeChildren
|
|
194
|
+
? target.GetComponentsInChildren<Collider>()
|
|
195
|
+
: target.GetComponents<Collider>();
|
|
196
|
+
foreach (var collider in colliders)
|
|
197
|
+
{
|
|
198
|
+
if (!hasBounds) { bounds = collider.bounds; hasBounds = true; }
|
|
199
|
+
else bounds.Encapsulate(collider.bounds);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return hasBounds;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
internal static List<object> Vec3(Vector3 v) => new List<object> { v.x, v.y, v.z };
|
|
206
|
+
}
|
|
207
|
+
}
|