@mflrevan/ucp 0.6.0 → 0.6.1
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/Bridge/BridgeServer.cs +36 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +137 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs +80 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +24 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +82 -23
- package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +409 -63
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +68 -6
- package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs +221 -0
- package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs.meta +2 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- package/package.json +1 -1
|
@@ -28,7 +28,13 @@ namespace UCP.Bridge
|
|
|
28
28
|
|
|
29
29
|
private sealed class Parser : IDisposable
|
|
30
30
|
{
|
|
31
|
+
/// Guards against stack overflow on deeply nested input. A stack overflow is not
|
|
32
|
+
/// catchable in .NET and kills the editor process outright, so the parser trades
|
|
33
|
+
/// pathological depth for a normal, catchable exception.
|
|
34
|
+
private const int MaxParseDepth = 256;
|
|
35
|
+
|
|
31
36
|
private StringReader _reader;
|
|
37
|
+
private int _depth;
|
|
32
38
|
|
|
33
39
|
private Parser(string jsonString)
|
|
34
40
|
{
|
|
@@ -78,16 +84,24 @@ namespace UCP.Bridge
|
|
|
78
84
|
ReadChar(); // {
|
|
79
85
|
var dict = new Dictionary<string, object>();
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
EnterContainer();
|
|
88
|
+
try
|
|
82
89
|
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
while (true)
|
|
91
|
+
{
|
|
92
|
+
EatWhitespace();
|
|
93
|
+
if (PeekChar() == '}') { ReadChar(); return dict; }
|
|
94
|
+
if (PeekChar() == ',') { ReadChar(); continue; }
|
|
95
|
+
|
|
96
|
+
var key = ParseString();
|
|
97
|
+
EatWhitespace();
|
|
98
|
+
ReadChar(); // :
|
|
99
|
+
dict[key] = ParseValue();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finally
|
|
103
|
+
{
|
|
104
|
+
_depth--;
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -96,16 +110,31 @@ namespace UCP.Bridge
|
|
|
96
110
|
ReadChar(); // [
|
|
97
111
|
var list = new List<object>();
|
|
98
112
|
|
|
99
|
-
|
|
113
|
+
EnterContainer();
|
|
114
|
+
try
|
|
100
115
|
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
while (true)
|
|
117
|
+
{
|
|
118
|
+
EatWhitespace();
|
|
119
|
+
if (PeekChar() == ']') { ReadChar(); return list; }
|
|
120
|
+
if (PeekChar() == ',') { ReadChar(); continue; }
|
|
104
121
|
|
|
105
|
-
|
|
122
|
+
list.Add(ParseValue());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally
|
|
126
|
+
{
|
|
127
|
+
_depth--;
|
|
106
128
|
}
|
|
107
129
|
}
|
|
108
130
|
|
|
131
|
+
private void EnterContainer()
|
|
132
|
+
{
|
|
133
|
+
if (_depth >= MaxParseDepth)
|
|
134
|
+
throw new FormatException($"JSON nesting exceeds {MaxParseDepth} levels");
|
|
135
|
+
_depth++;
|
|
136
|
+
}
|
|
137
|
+
|
|
109
138
|
private string ParseString()
|
|
110
139
|
{
|
|
111
140
|
ReadChar(); // opening "
|
|
@@ -200,16 +229,45 @@ namespace UCP.Bridge
|
|
|
200
229
|
return c < 0 ? '\0' : (char)c;
|
|
201
230
|
}
|
|
202
231
|
|
|
232
|
+
/// Truncated input used to yield '\0' forever, which spun ParseString in an infinite
|
|
233
|
+
/// loop and hung the editor's main thread. Fail loudly at end-of-input instead.
|
|
203
234
|
private char ReadChar()
|
|
204
235
|
{
|
|
205
236
|
int c = _reader.Read();
|
|
206
|
-
|
|
237
|
+
if (c < 0) throw new FormatException("Unexpected end of JSON input");
|
|
238
|
+
return (char)c;
|
|
207
239
|
}
|
|
208
240
|
}
|
|
209
241
|
|
|
210
242
|
private sealed class Serializer
|
|
211
243
|
{
|
|
244
|
+
/// <summary>
|
|
245
|
+
/// Reflection is the dangerous path: computed properties can hand back fresh instances
|
|
246
|
+
/// of their own type forever (UnityEngine.Vector3.normalized is the canonical example),
|
|
247
|
+
/// so an unbounded walk stack-overflows -- which is not catchable in .NET and takes the
|
|
248
|
+
/// whole editor process down with it. Bound it.
|
|
249
|
+
/// </summary>
|
|
250
|
+
private const int MaxReflectionDepth = 8;
|
|
251
|
+
|
|
252
|
+
/// Backstop for container nesting (dictionaries/lists). Reference cycles are caught
|
|
253
|
+
/// separately, so this only fires on genuinely pathological payloads.
|
|
254
|
+
private const int MaxDepth = 96;
|
|
255
|
+
|
|
256
|
+
/// Total reflected objects per payload, to bound fan-out: a type whose properties each
|
|
257
|
+
/// return new instances of a similar type grows exponentially, not linearly, with depth.
|
|
258
|
+
private const int MaxReflectedObjects = 20000;
|
|
259
|
+
|
|
260
|
+
private const string MaxDepthMarker = "<ucp:max-depth>";
|
|
261
|
+
private const string CycleMarker = "<ucp:cycle>";
|
|
262
|
+
private const string TruncatedMarker = "<ucp:truncated>";
|
|
263
|
+
|
|
264
|
+
private static readonly CultureInfo Inv = CultureInfo.InvariantCulture;
|
|
265
|
+
|
|
212
266
|
private readonly StringBuilder _sb = new();
|
|
267
|
+
private readonly HashSet<object> _visited = new(ReferenceComparer.Instance);
|
|
268
|
+
private int _depth;
|
|
269
|
+
private int _reflectionDepth;
|
|
270
|
+
private int _reflectedObjects;
|
|
213
271
|
|
|
214
272
|
public static string Serialize(object obj)
|
|
215
273
|
{
|
|
@@ -224,43 +282,270 @@ namespace UCP.Bridge
|
|
|
224
282
|
{
|
|
225
283
|
case null:
|
|
226
284
|
_sb.Append("null");
|
|
227
|
-
|
|
285
|
+
return;
|
|
228
286
|
case string s:
|
|
229
287
|
WriteString(s);
|
|
230
|
-
|
|
288
|
+
return;
|
|
231
289
|
case bool b:
|
|
232
290
|
_sb.Append(b ? "true" : "false");
|
|
233
|
-
|
|
234
|
-
case IDictionary dict:
|
|
235
|
-
WriteDict(dict);
|
|
236
|
-
break;
|
|
237
|
-
case IList list:
|
|
238
|
-
WriteArray(list);
|
|
239
|
-
break;
|
|
291
|
+
return;
|
|
240
292
|
case float f:
|
|
241
|
-
|
|
242
|
-
|
|
293
|
+
WriteFloat(f);
|
|
294
|
+
return;
|
|
243
295
|
case double d:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
case
|
|
247
|
-
_sb.Append(
|
|
248
|
-
|
|
249
|
-
case long l:
|
|
250
|
-
_sb.Append(l);
|
|
251
|
-
break;
|
|
296
|
+
WriteDouble(d);
|
|
297
|
+
return;
|
|
298
|
+
case decimal m:
|
|
299
|
+
_sb.Append(m.ToString(Inv));
|
|
300
|
+
return;
|
|
252
301
|
case Enum e:
|
|
253
|
-
|
|
254
|
-
|
|
302
|
+
WriteEnum(e);
|
|
303
|
+
return;
|
|
304
|
+
case byte v:
|
|
305
|
+
_sb.Append(((int)v).ToString(Inv));
|
|
306
|
+
return;
|
|
307
|
+
case sbyte v:
|
|
308
|
+
_sb.Append(((int)v).ToString(Inv));
|
|
309
|
+
return;
|
|
310
|
+
case short v:
|
|
311
|
+
_sb.Append(((int)v).ToString(Inv));
|
|
312
|
+
return;
|
|
313
|
+
case ushort v:
|
|
314
|
+
_sb.Append(((int)v).ToString(Inv));
|
|
315
|
+
return;
|
|
316
|
+
case int v:
|
|
317
|
+
_sb.Append(v.ToString(Inv));
|
|
318
|
+
return;
|
|
319
|
+
case uint v:
|
|
320
|
+
_sb.Append(v.ToString(Inv));
|
|
321
|
+
return;
|
|
322
|
+
case long v:
|
|
323
|
+
_sb.Append(v.ToString(Inv));
|
|
324
|
+
return;
|
|
325
|
+
case ulong v:
|
|
326
|
+
_sb.Append(v.ToString(Inv));
|
|
327
|
+
return;
|
|
328
|
+
case char c:
|
|
329
|
+
WriteString(c.ToString());
|
|
330
|
+
return;
|
|
331
|
+
case DateTime dt:
|
|
332
|
+
WriteString(dt.ToString("o", Inv));
|
|
333
|
+
return;
|
|
334
|
+
case DateTimeOffset dto:
|
|
335
|
+
WriteString(dto.ToString("o", Inv));
|
|
336
|
+
return;
|
|
337
|
+
case TimeSpan ts:
|
|
338
|
+
WriteString(ts.ToString(null, Inv));
|
|
339
|
+
return;
|
|
340
|
+
case Guid g:
|
|
341
|
+
WriteString(g.ToString());
|
|
342
|
+
return;
|
|
343
|
+
case Type t:
|
|
344
|
+
WriteString(t.FullName);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Unity's math structs expose self-referential computed properties
|
|
349
|
+
// (Vector3.normalized, Quaternion.normalized, Bounds.extents, ...) plus indexers.
|
|
350
|
+
// Reflecting over them is what crashed the editor, so give them explicit shapes.
|
|
351
|
+
if (TryWriteUnityValue(obj)) return;
|
|
352
|
+
|
|
353
|
+
// UnityEngine.Object graphs are cyclic by construction
|
|
354
|
+
// (GameObject.transform.gameObject), reach the entire scene, and carry no useful
|
|
355
|
+
// JSON projection. Emit an identity instead of walking them.
|
|
356
|
+
if (obj is UnityEngine.Object uo)
|
|
357
|
+
{
|
|
358
|
+
WriteUnityObject(uo);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Everything below here recurses, so it needs the depth and cycle guards.
|
|
363
|
+
if (_depth >= MaxDepth)
|
|
364
|
+
{
|
|
365
|
+
WriteString(MaxDepthMarker);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
var track = !obj.GetType().IsValueType;
|
|
370
|
+
if (track && !_visited.Add(obj))
|
|
371
|
+
{
|
|
372
|
+
WriteString(CycleMarker);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_depth++;
|
|
377
|
+
try
|
|
378
|
+
{
|
|
379
|
+
switch (obj)
|
|
380
|
+
{
|
|
381
|
+
case IDictionary dict:
|
|
382
|
+
WriteDict(dict);
|
|
383
|
+
break;
|
|
384
|
+
case IList list:
|
|
385
|
+
WriteArray(list);
|
|
386
|
+
break;
|
|
387
|
+
case IEnumerable seq:
|
|
388
|
+
WriteEnumerable(seq);
|
|
389
|
+
break;
|
|
390
|
+
default:
|
|
391
|
+
WriteObject(obj);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
finally
|
|
396
|
+
{
|
|
397
|
+
_depth--;
|
|
398
|
+
if (track) _visited.Remove(obj);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private void WriteEnum(Enum e)
|
|
403
|
+
{
|
|
404
|
+
try
|
|
405
|
+
{
|
|
406
|
+
if (Enum.GetUnderlyingType(e.GetType()) == typeof(ulong))
|
|
407
|
+
_sb.Append(Convert.ToUInt64(e).ToString(Inv));
|
|
408
|
+
else
|
|
409
|
+
_sb.Append(Convert.ToInt64(e).ToString(Inv));
|
|
410
|
+
}
|
|
411
|
+
catch
|
|
412
|
+
{
|
|
413
|
+
WriteString(e.ToString());
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/// NaN and Infinity are not valid JSON; emitting them raw yields a payload the CLI
|
|
418
|
+
/// cannot parse. Unity produces them routinely (degenerate bounds, zero-length
|
|
419
|
+
/// normalize, uninitialized transforms).
|
|
420
|
+
private void WriteFloat(float f)
|
|
421
|
+
{
|
|
422
|
+
if (float.IsNaN(f) || float.IsInfinity(f)) _sb.Append("null");
|
|
423
|
+
else _sb.Append(f.ToString("R", Inv));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private void WriteDouble(double d)
|
|
427
|
+
{
|
|
428
|
+
if (double.IsNaN(d) || double.IsInfinity(d)) _sb.Append("null");
|
|
429
|
+
else _sb.Append(d.ToString("R", Inv));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private bool TryWriteUnityValue(object obj)
|
|
433
|
+
{
|
|
434
|
+
switch (obj)
|
|
435
|
+
{
|
|
436
|
+
case UnityEngine.Vector2 v:
|
|
437
|
+
WriteFloats(("x", v.x), ("y", v.y));
|
|
438
|
+
return true;
|
|
439
|
+
case UnityEngine.Vector3 v:
|
|
440
|
+
WriteFloats(("x", v.x), ("y", v.y), ("z", v.z));
|
|
441
|
+
return true;
|
|
442
|
+
case UnityEngine.Vector4 v:
|
|
443
|
+
WriteFloats(("x", v.x), ("y", v.y), ("z", v.z), ("w", v.w));
|
|
444
|
+
return true;
|
|
445
|
+
case UnityEngine.Quaternion q:
|
|
446
|
+
WriteFloats(("x", q.x), ("y", q.y), ("z", q.z), ("w", q.w));
|
|
447
|
+
return true;
|
|
448
|
+
case UnityEngine.Color c:
|
|
449
|
+
WriteFloats(("r", c.r), ("g", c.g), ("b", c.b), ("a", c.a));
|
|
450
|
+
return true;
|
|
451
|
+
case UnityEngine.Color32 c:
|
|
452
|
+
WriteFloats(("r", c.r), ("g", c.g), ("b", c.b), ("a", c.a));
|
|
453
|
+
return true;
|
|
454
|
+
case UnityEngine.Vector2Int v:
|
|
455
|
+
WriteFloats(("x", v.x), ("y", v.y));
|
|
456
|
+
return true;
|
|
457
|
+
case UnityEngine.Vector3Int v:
|
|
458
|
+
WriteFloats(("x", v.x), ("y", v.y), ("z", v.z));
|
|
459
|
+
return true;
|
|
460
|
+
case UnityEngine.Rect r:
|
|
461
|
+
WriteFloats(("x", r.x), ("y", r.y), ("width", r.width), ("height", r.height));
|
|
462
|
+
return true;
|
|
463
|
+
case UnityEngine.RectInt r:
|
|
464
|
+
WriteFloats(("x", r.x), ("y", r.y), ("width", r.width), ("height", r.height));
|
|
465
|
+
return true;
|
|
466
|
+
case UnityEngine.Bounds b:
|
|
467
|
+
WriteBounds(b.center, b.size);
|
|
468
|
+
return true;
|
|
469
|
+
case UnityEngine.BoundsInt b:
|
|
470
|
+
WriteBounds(b.center, b.size);
|
|
471
|
+
return true;
|
|
472
|
+
case UnityEngine.Matrix4x4 mtx:
|
|
473
|
+
_sb.Append('[');
|
|
474
|
+
for (int i = 0; i < 16; i++)
|
|
475
|
+
{
|
|
476
|
+
if (i > 0) _sb.Append(',');
|
|
477
|
+
WriteFloat(mtx[i]);
|
|
478
|
+
}
|
|
479
|
+
_sb.Append(']');
|
|
480
|
+
return true;
|
|
255
481
|
default:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private void WriteFloats(params (string Name, float Value)[] members)
|
|
487
|
+
{
|
|
488
|
+
_sb.Append('{');
|
|
489
|
+
for (int i = 0; i < members.Length; i++)
|
|
490
|
+
{
|
|
491
|
+
if (i > 0) _sb.Append(',');
|
|
492
|
+
WriteString(members[i].Name);
|
|
493
|
+
_sb.Append(':');
|
|
494
|
+
WriteFloat(members[i].Value);
|
|
495
|
+
}
|
|
496
|
+
_sb.Append('}');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private void WriteBounds(UnityEngine.Vector3 center, UnityEngine.Vector3 size)
|
|
500
|
+
{
|
|
501
|
+
_sb.Append("{\"center\":");
|
|
502
|
+
WriteFloats(("x", center.x), ("y", center.y), ("z", center.z));
|
|
503
|
+
_sb.Append(",\"size\":");
|
|
504
|
+
WriteFloats(("x", size.x), ("y", size.y), ("z", size.z));
|
|
505
|
+
_sb.Append('}');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private void WriteUnityObject(UnityEngine.Object uo)
|
|
509
|
+
{
|
|
510
|
+
// Unity's overloaded == reports destroyed objects as null even though the managed
|
|
511
|
+
// reference is alive, and touching .name on those throws.
|
|
512
|
+
if (uo == null)
|
|
513
|
+
{
|
|
514
|
+
_sb.Append("null");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
string name;
|
|
519
|
+
int id;
|
|
520
|
+
try
|
|
521
|
+
{
|
|
522
|
+
name = uo.name;
|
|
523
|
+
id = uo.GetId();
|
|
259
524
|
}
|
|
525
|
+
catch
|
|
526
|
+
{
|
|
527
|
+
_sb.Append("null");
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
_sb.Append('{');
|
|
532
|
+
WriteString("name");
|
|
533
|
+
_sb.Append(':');
|
|
534
|
+
WriteString(name);
|
|
535
|
+
_sb.Append(",\"instanceId\":");
|
|
536
|
+
_sb.Append(id.ToString(Inv));
|
|
537
|
+
_sb.Append(",\"type\":");
|
|
538
|
+
WriteString(uo.GetType().Name);
|
|
539
|
+
_sb.Append('}');
|
|
260
540
|
}
|
|
261
541
|
|
|
262
542
|
private void WriteString(string s)
|
|
263
543
|
{
|
|
544
|
+
if (s == null)
|
|
545
|
+
{
|
|
546
|
+
_sb.Append("null");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
264
549
|
_sb.Append('"');
|
|
265
550
|
foreach (var c in s)
|
|
266
551
|
{
|
|
@@ -292,7 +577,8 @@ namespace UCP.Bridge
|
|
|
292
577
|
{
|
|
293
578
|
if (!first) _sb.Append(',');
|
|
294
579
|
first = false;
|
|
295
|
-
|
|
580
|
+
// A key must always be a quoted string, even if ToString() yields null.
|
|
581
|
+
WriteString(entry.Key?.ToString() ?? string.Empty);
|
|
296
582
|
_sb.Append(':');
|
|
297
583
|
WriteValue(entry.Value);
|
|
298
584
|
}
|
|
@@ -310,7 +596,45 @@ namespace UCP.Bridge
|
|
|
310
596
|
_sb.Append(']');
|
|
311
597
|
}
|
|
312
598
|
|
|
599
|
+
private void WriteEnumerable(IEnumerable seq)
|
|
600
|
+
{
|
|
601
|
+
_sb.Append('[');
|
|
602
|
+
bool first = true;
|
|
603
|
+
foreach (var item in seq)
|
|
604
|
+
{
|
|
605
|
+
if (!first) _sb.Append(',');
|
|
606
|
+
first = false;
|
|
607
|
+
WriteValue(item);
|
|
608
|
+
}
|
|
609
|
+
_sb.Append(']');
|
|
610
|
+
}
|
|
611
|
+
|
|
313
612
|
private void WriteObject(object obj)
|
|
613
|
+
{
|
|
614
|
+
if (_reflectionDepth >= MaxReflectionDepth)
|
|
615
|
+
{
|
|
616
|
+
WriteString(MaxDepthMarker);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (_reflectedObjects >= MaxReflectedObjects)
|
|
620
|
+
{
|
|
621
|
+
WriteString(TruncatedMarker);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
_reflectedObjects++;
|
|
626
|
+
_reflectionDepth++;
|
|
627
|
+
try
|
|
628
|
+
{
|
|
629
|
+
WriteObjectMembers(obj);
|
|
630
|
+
}
|
|
631
|
+
finally
|
|
632
|
+
{
|
|
633
|
+
_reflectionDepth--;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private void WriteObjectMembers(object obj)
|
|
314
638
|
{
|
|
315
639
|
var type = obj.GetType();
|
|
316
640
|
var props = type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
|
@@ -322,37 +646,59 @@ namespace UCP.Bridge
|
|
|
322
646
|
foreach (var prop in props)
|
|
323
647
|
{
|
|
324
648
|
if (!prop.CanRead) continue;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
first = false;
|
|
330
|
-
// Convert PascalCase to camelCase
|
|
331
|
-
var name = char.ToLowerInvariant(prop.Name[0]) + prop.Name.Substring(1);
|
|
332
|
-
WriteString(name);
|
|
333
|
-
_sb.Append(':');
|
|
334
|
-
WriteValue(val);
|
|
335
|
-
}
|
|
336
|
-
catch { }
|
|
649
|
+
// Indexers (Vector3's this[int], IList's this[int], ...) have no readable value.
|
|
650
|
+
if (prop.GetIndexParameters().Length > 0) continue;
|
|
651
|
+
var captured = prop;
|
|
652
|
+
WriteMember(captured.Name, () => captured.GetValue(obj), ref first);
|
|
337
653
|
}
|
|
338
654
|
|
|
339
655
|
foreach (var field in fields)
|
|
340
656
|
{
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
var val = field.GetValue(obj);
|
|
344
|
-
if (!first) _sb.Append(',');
|
|
345
|
-
first = false;
|
|
346
|
-
var name = char.ToLowerInvariant(field.Name[0]) + field.Name.Substring(1);
|
|
347
|
-
WriteString(name);
|
|
348
|
-
_sb.Append(':');
|
|
349
|
-
WriteValue(val);
|
|
350
|
-
}
|
|
351
|
-
catch { }
|
|
657
|
+
var captured = field;
|
|
658
|
+
WriteMember(captured.Name, () => captured.GetValue(obj), ref first);
|
|
352
659
|
}
|
|
353
660
|
|
|
354
661
|
_sb.Append('}');
|
|
355
662
|
}
|
|
663
|
+
|
|
664
|
+
/// Writes one member, rolling the buffer back if reading or serializing it throws.
|
|
665
|
+
/// The separator and key used to be appended before the value was evaluated, so a
|
|
666
|
+
/// throwing getter left a dangling `"key":` behind and produced invalid JSON.
|
|
667
|
+
private void WriteMember(string name, Func<object> read, ref bool first)
|
|
668
|
+
{
|
|
669
|
+
var rollback = _sb.Length;
|
|
670
|
+
var wasFirst = first;
|
|
671
|
+
try
|
|
672
|
+
{
|
|
673
|
+
var val = read();
|
|
674
|
+
if (!first) _sb.Append(',');
|
|
675
|
+
first = false;
|
|
676
|
+
// Convert PascalCase to camelCase
|
|
677
|
+
WriteString(string.IsNullOrEmpty(name)
|
|
678
|
+
? name
|
|
679
|
+
: char.ToLowerInvariant(name[0]) + name.Substring(1));
|
|
680
|
+
_sb.Append(':');
|
|
681
|
+
WriteValue(val);
|
|
682
|
+
}
|
|
683
|
+
catch
|
|
684
|
+
{
|
|
685
|
+
_sb.Length = rollback;
|
|
686
|
+
first = wasFirst;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/// Identity comparer for cycle detection: value equality would collapse distinct but
|
|
691
|
+
/// equal nodes, and a payload type's own Equals/GetHashCode can be arbitrarily
|
|
692
|
+
/// expensive or throw.
|
|
693
|
+
private sealed class ReferenceComparer : IEqualityComparer<object>
|
|
694
|
+
{
|
|
695
|
+
public static readonly ReferenceComparer Instance = new();
|
|
696
|
+
|
|
697
|
+
public new bool Equals(object x, object y) => ReferenceEquals(x, y);
|
|
698
|
+
|
|
699
|
+
public int GetHashCode(object obj) =>
|
|
700
|
+
System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
|
|
701
|
+
}
|
|
356
702
|
}
|
|
357
703
|
}
|
|
358
704
|
}
|
|
@@ -307,10 +307,48 @@ namespace UCP.Bridge.Tests
|
|
|
307
307
|
var status = _router.Dispatch("logs/status", 1, "{}");
|
|
308
308
|
Assert.That(status.error, Is.Null);
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
Assert
|
|
313
|
-
|
|
310
|
+
Assert.That(status.result, Is.InstanceOf<Dictionary<string, object>>());
|
|
311
|
+
|
|
312
|
+
// Assert on the specific regression this test exists for -- `asset/search` loading
|
|
313
|
+
// `.unity` files through LoadAllAssetsAtPath, which makes Unity emit
|
|
314
|
+
// "Do not use ReadObjectThreaded on scene objects!" -- rather than on "zero errors of
|
|
315
|
+
// any kind". A blanket count also catches unrelated editor noise: on a cold Library the
|
|
316
|
+
// search pulls in lazy imports whose URP shader-fallback errors have nothing to do with
|
|
317
|
+
// this code path, which made the whole release matrix red on every Unity version.
|
|
318
|
+
var problems = BufferedProblems();
|
|
319
|
+
var threadedReadErrors = problems
|
|
320
|
+
.FindAll(entry => entry.Contains("ReadObjectThreaded"));
|
|
321
|
+
|
|
322
|
+
Assert.That(
|
|
323
|
+
threadedReadErrors,
|
|
324
|
+
Is.Empty,
|
|
325
|
+
() => "asset/search emitted threaded scene-read errors:" + NewLineIndent
|
|
326
|
+
+ string.Join(NewLineIndent, threadedReadErrors));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private const string NewLineIndent = "\n ";
|
|
330
|
+
|
|
331
|
+
/// Buffered error/exception entries, as "[level] message" strings.
|
|
332
|
+
private List<string> BufferedProblems()
|
|
333
|
+
{
|
|
334
|
+
var lines = new List<string>();
|
|
335
|
+
var tail = _router.Dispatch("logs/tail", 1, "{\"count\":200}");
|
|
336
|
+
if (tail.error != null || tail.result is not Dictionary<string, object> payload)
|
|
337
|
+
return lines;
|
|
338
|
+
|
|
339
|
+
if (!payload.TryGetValue("logs", out var logsObj) || logsObj is not List<object> logs)
|
|
340
|
+
return lines;
|
|
341
|
+
|
|
342
|
+
foreach (var item in logs)
|
|
343
|
+
{
|
|
344
|
+
if (item is not Dictionary<string, object> entry) continue;
|
|
345
|
+
var level = entry.TryGetValue("level", out var l) ? l?.ToString() : null;
|
|
346
|
+
if (level != "error" && level != "exception") continue;
|
|
347
|
+
var message = entry.TryGetValue("messagePreview", out var m) ? m?.ToString() : "";
|
|
348
|
+
lines.Add($"[{level}] {message}");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return lines;
|
|
314
352
|
}
|
|
315
353
|
|
|
316
354
|
[Test]
|
|
@@ -1142,9 +1180,33 @@ namespace UCP.Bridge.Tests
|
|
|
1142
1180
|
System.Convert.ToSingle(axisData[0]),
|
|
1143
1181
|
System.Convert.ToSingle(axisData[1]),
|
|
1144
1182
|
System.Convert.ToSingle(axisData[2]));
|
|
1145
|
-
var actualForward = sceneView.camera.transform.forward;
|
|
1146
1183
|
Assert.That(Vector3.Dot(returnedAxis.normalized, expectedDirection), Is.GreaterThan(0.98f));
|
|
1147
|
-
|
|
1184
|
+
|
|
1185
|
+
// Assert against the Scene view's own state and the reported pose, not
|
|
1186
|
+
// `sceneView.camera.transform`: that transform is only synced when the view repaints,
|
|
1187
|
+
// so in batch mode it still holds the pre-focus pose and this check used to fail.
|
|
1188
|
+
var viewForward = sceneView.rotation * Vector3.forward;
|
|
1189
|
+
Assert.That(Mathf.Abs(Vector3.Dot(viewForward.normalized, expectedDirection)), Is.GreaterThan(0.98f));
|
|
1190
|
+
|
|
1191
|
+
var eulerData = (List<object>)result["cameraRotationEuler"];
|
|
1192
|
+
var reportedForward = Quaternion.Euler(
|
|
1193
|
+
System.Convert.ToSingle(eulerData[0]),
|
|
1194
|
+
System.Convert.ToSingle(eulerData[1]),
|
|
1195
|
+
System.Convert.ToSingle(eulerData[2])) * Vector3.forward;
|
|
1196
|
+
Assert.That(
|
|
1197
|
+
Mathf.Abs(Vector3.Dot(reportedForward.normalized, expectedDirection)),
|
|
1198
|
+
Is.GreaterThan(0.98f),
|
|
1199
|
+
"scene/focus must report the pose it just applied, not the last rendered one");
|
|
1200
|
+
|
|
1201
|
+
// The reported camera must sit behind the pivot along its own forward axis.
|
|
1202
|
+
var positionData = (List<object>)result["cameraPosition"];
|
|
1203
|
+
var reportedPosition = new Vector3(
|
|
1204
|
+
System.Convert.ToSingle(positionData[0]),
|
|
1205
|
+
System.Convert.ToSingle(positionData[1]),
|
|
1206
|
+
System.Convert.ToSingle(positionData[2]));
|
|
1207
|
+
Assert.That(Vector3.Dot((sceneView.pivot - reportedPosition).normalized, reportedForward.normalized),
|
|
1208
|
+
Is.GreaterThan(0.98f));
|
|
1209
|
+
|
|
1148
1210
|
Assert.That(Vector3.Distance(sceneView.pivot, cube.transform.position), Is.LessThan(2f));
|
|
1149
1211
|
}
|
|
1150
1212
|
|