@mflrevan/ucp 0.5.2 → 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/AssemblyInfo.cs +3 -0
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +45 -5
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +137 -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/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 +1 -35
- package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs +80 -5
- 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 +26 -36
- package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +82 -23
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +4 -4
- 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/Editor/Protocol/MiniJson.cs +409 -63
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +131 -13
- 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/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
|
@@ -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
|
}
|