dominus-cli 0.6.0 → 2.1.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/CHANGELOG.md +175 -0
- package/README.md +164 -248
- package/dist/install-plugin.js +30 -0
- package/dist/install-plugin.js.map +1 -0
- package/dist/mcp.js +1576 -1346
- package/dist/mcp.js.map +1 -1
- package/docs/DOMINUS_2_PLAN.md +75 -0
- package/package.json +24 -25
- package/plugin/Dominus.rbxm +0 -0
- package/plugin/src/CommandRouter.lua +94 -0
- package/plugin/src/Explorer.lua +530 -373
- package/plugin/src/InstanceRegistry.lua +122 -0
- package/plugin/src/Mutation.lua +135 -0
- package/plugin/src/Properties.lua +859 -725
- package/plugin/src/Protocol.lua +30 -30
- package/plugin/src/Reflection.lua +91 -59
- package/plugin/src/TestRunner.lua +69 -141
- package/plugin/src/UIBuilder.lua +857 -727
- package/plugin/src/ValueCodec.lua +230 -0
- package/plugin/src/Watcher.lua +85 -85
- package/plugin/src/WsClient.lua +153 -81
- package/plugin/src/init.server.lua +175 -362
- package/dist/index.js +0 -5796
- package/dist/index.js.map +0 -1
- package/plugin/src/Executor.lua +0 -117
|
@@ -1,725 +1,859 @@
|
|
|
1
|
-
--[[
|
|
2
|
-
Properties module: read/write instance properties
|
|
3
|
-
Uses ReflectionService to determine expected types for smart coercion.
|
|
4
|
-
]]
|
|
5
|
-
|
|
6
|
-
local Explorer = require(script.Parent.Explorer)
|
|
7
|
-
local
|
|
8
|
-
|
|
9
|
-
local
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
["
|
|
73
|
-
[""] = true,
|
|
74
|
-
["
|
|
75
|
-
["
|
|
76
|
-
["
|
|
77
|
-
["Enum.
|
|
78
|
-
["Enum.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
value.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
value.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if type(value) == "
|
|
347
|
-
return UDim.new(value, 0)
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if value:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if type(value) == "
|
|
423
|
-
return BrickColor.new(value)
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
value[
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if type(value) == "
|
|
479
|
-
return NumberRange.new(value)
|
|
480
|
-
end
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return NumberSequence.new(
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
return Font.new(
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
value.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if value
|
|
596
|
-
return Vector3.new(value
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
end
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
end
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
local
|
|
659
|
-
local
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
end
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1
|
+
--[[
|
|
2
|
+
Properties module: read/write instance properties
|
|
3
|
+
Uses ReflectionService to determine expected types for smart coercion.
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local Explorer = require(script.Parent.Explorer)
|
|
7
|
+
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
8
|
+
local Reflection = require(script.Parent.Reflection)
|
|
9
|
+
local ValueCodec = require(script.Parent.ValueCodec)
|
|
10
|
+
local ReflectionService = game:GetService("ReflectionService")
|
|
11
|
+
|
|
12
|
+
local Properties = {}
|
|
13
|
+
|
|
14
|
+
local IGNORED_PROPERTIES = {
|
|
15
|
+
Parent = true,
|
|
16
|
+
ClassName = true,
|
|
17
|
+
Archivable = true,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
-- Properties that are read-only / computed / redundant — strip in compact mode
|
|
21
|
+
local COMPACT_SKIP = {
|
|
22
|
+
-- Read-only computed
|
|
23
|
+
AbsolutePosition = true,
|
|
24
|
+
AbsoluteSize = true,
|
|
25
|
+
AbsoluteRotation = true,
|
|
26
|
+
AbsoluteCanvasSize = true,
|
|
27
|
+
TextBounds = true,
|
|
28
|
+
TextFits = true,
|
|
29
|
+
IsLoaded = true,
|
|
30
|
+
ContentText = true,
|
|
31
|
+
LocalizedText = true,
|
|
32
|
+
GuiState = true,
|
|
33
|
+
-- Deprecated / redundant (BrickColor duplicates Color3, Font duplicates FontFace)
|
|
34
|
+
BackgroundColor = true,
|
|
35
|
+
BorderColor = true,
|
|
36
|
+
TextColor = true,
|
|
37
|
+
Font = true,
|
|
38
|
+
FontSize = true,
|
|
39
|
+
-- Internal / security
|
|
40
|
+
Capabilities = true,
|
|
41
|
+
Sandboxed = true,
|
|
42
|
+
archivable = true,
|
|
43
|
+
className = true,
|
|
44
|
+
Localize = true,
|
|
45
|
+
-- Selection navigation (almost always nil/default)
|
|
46
|
+
NextSelectionDown = true,
|
|
47
|
+
NextSelectionLeft = true,
|
|
48
|
+
NextSelectionRight = true,
|
|
49
|
+
NextSelectionUp = true,
|
|
50
|
+
SelectionBehaviorDown = true,
|
|
51
|
+
SelectionBehaviorLeft = true,
|
|
52
|
+
SelectionBehaviorRight = true,
|
|
53
|
+
SelectionBehaviorUp = true,
|
|
54
|
+
SelectionGroup = true,
|
|
55
|
+
SelectionOrder = true,
|
|
56
|
+
SelectionImageObject = true,
|
|
57
|
+
-- Rarely useful defaults
|
|
58
|
+
Draggable = true,
|
|
59
|
+
InputSink = true,
|
|
60
|
+
RootLocalizationTable = true,
|
|
61
|
+
AutoLocalize = true,
|
|
62
|
+
OpenTypeFeatures = true,
|
|
63
|
+
OpenTypeFeaturesError = true,
|
|
64
|
+
Interactable = true,
|
|
65
|
+
Style = true,
|
|
66
|
+
BorderMode = true,
|
|
67
|
+
SizeConstraint = true,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
-- Default values that are not worth reporting in compact mode
|
|
71
|
+
local COMPACT_DEFAULT_VALUES = {
|
|
72
|
+
["0"] = true,
|
|
73
|
+
["1"] = true,
|
|
74
|
+
["false"] = true,
|
|
75
|
+
["nil"] = true,
|
|
76
|
+
[""] = true,
|
|
77
|
+
["Enum.AutomaticSize.None"] = true,
|
|
78
|
+
["Enum.TextTruncate.None"] = true,
|
|
79
|
+
["Enum.TextDirection.Auto"] = true,
|
|
80
|
+
["Enum.ResamplerMode.Default"] = true,
|
|
81
|
+
["Enum.ScaleType.Stretch"] = true,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
-- Cache property type maps per class to avoid repeated reflection calls
|
|
85
|
+
local typeCache = {}
|
|
86
|
+
|
|
87
|
+
local function getPropertyTypeMap(className)
|
|
88
|
+
if typeCache[className] then
|
|
89
|
+
return typeCache[className]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
local typeMap = {}
|
|
93
|
+
local ok, props = pcall(function()
|
|
94
|
+
return ReflectionService:GetPropertiesOfClass(className)
|
|
95
|
+
end)
|
|
96
|
+
if ok and props then
|
|
97
|
+
for _, prop in props do
|
|
98
|
+
if prop.Type then
|
|
99
|
+
typeMap[prop.Name] = tostring(prop.Type)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
typeCache[className] = typeMap
|
|
104
|
+
return typeMap
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
function Properties.get(path, compact)
|
|
108
|
+
local instance = Explorer.resolveInstance(path)
|
|
109
|
+
if not instance then
|
|
110
|
+
return { success = false, error = "Instance not found: " .. path }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
local properties = {}
|
|
114
|
+
|
|
115
|
+
local ok, apiData = pcall(function()
|
|
116
|
+
local info = {}
|
|
117
|
+
local propData = ReflectionService:GetPropertiesOfClass(instance.ClassName)
|
|
118
|
+
for _, prop in propData do
|
|
119
|
+
if not IGNORED_PROPERTIES[prop.Name] then
|
|
120
|
+
if compact and COMPACT_SKIP[prop.Name] then
|
|
121
|
+
continue
|
|
122
|
+
end
|
|
123
|
+
local valueOk, value = pcall(function()
|
|
124
|
+
return instance[prop.Name]
|
|
125
|
+
end)
|
|
126
|
+
if valueOk then
|
|
127
|
+
local valueStr = tostring(value)
|
|
128
|
+
if compact and valueStr == "nil" then
|
|
129
|
+
continue
|
|
130
|
+
end
|
|
131
|
+
table.insert(info, {
|
|
132
|
+
name = prop.Name,
|
|
133
|
+
value = valueStr,
|
|
134
|
+
type = prop.Type and tostring(prop.Type) or typeof(value),
|
|
135
|
+
category = prop.Display and prop.Display.Category or "Other",
|
|
136
|
+
})
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
return info
|
|
141
|
+
end)
|
|
142
|
+
|
|
143
|
+
if ok then
|
|
144
|
+
properties = apiData
|
|
145
|
+
else
|
|
146
|
+
local commonProps =
|
|
147
|
+
{ "Name", "Position", "Size", "Color", "Material", "Transparency", "Anchored", "CanCollide" }
|
|
148
|
+
for _, propName in commonProps do
|
|
149
|
+
local valOk, val = pcall(function()
|
|
150
|
+
return instance[propName]
|
|
151
|
+
end)
|
|
152
|
+
if valOk and val ~= nil then
|
|
153
|
+
table.insert(properties, {
|
|
154
|
+
name = propName,
|
|
155
|
+
value = tostring(val),
|
|
156
|
+
type = typeof(val),
|
|
157
|
+
})
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
return { properties = properties }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
function Properties.getDescendants(path, compact)
|
|
166
|
+
local rootInstance = Explorer.resolveInstance(path)
|
|
167
|
+
if not rootInstance then
|
|
168
|
+
return { success = false, error = "Instance not found: " .. path }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
-- In compact mode, skip noisy/default properties to reduce payload size
|
|
172
|
+
local shouldSkip = compact
|
|
173
|
+
and function(propName, valueStr)
|
|
174
|
+
if COMPACT_SKIP[propName] then
|
|
175
|
+
return true
|
|
176
|
+
end
|
|
177
|
+
-- Skip nil-valued properties
|
|
178
|
+
if valueStr == "nil" then
|
|
179
|
+
return true
|
|
180
|
+
end
|
|
181
|
+
return false
|
|
182
|
+
end
|
|
183
|
+
or function()
|
|
184
|
+
return false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
-- Extra compact filter: skip properties that are clearly defaults and non-informative
|
|
188
|
+
local isBoringDefault = compact
|
|
189
|
+
and function(propName, valueStr)
|
|
190
|
+
-- These specific properties at default values are noise
|
|
191
|
+
local boringAtDefault = {
|
|
192
|
+
Active = "false",
|
|
193
|
+
Selectable = "false",
|
|
194
|
+
ClipsDescendants = "false",
|
|
195
|
+
TextWrap = "false",
|
|
196
|
+
TextWrapped = "false",
|
|
197
|
+
RichText = "false",
|
|
198
|
+
TextScaled = "false",
|
|
199
|
+
Rotation = "0",
|
|
200
|
+
LayoutOrder = "0",
|
|
201
|
+
MaxVisibleGraphemes = "-1",
|
|
202
|
+
SliceScale = "1",
|
|
203
|
+
LineHeight = "1",
|
|
204
|
+
BorderSizePixel = "1",
|
|
205
|
+
Visible = "true",
|
|
206
|
+
}
|
|
207
|
+
return boringAtDefault[propName] == valueStr
|
|
208
|
+
end
|
|
209
|
+
or function()
|
|
210
|
+
return false
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
local function getProps(instance)
|
|
214
|
+
local properties = {}
|
|
215
|
+
|
|
216
|
+
local ok, apiData = pcall(function()
|
|
217
|
+
local info = {}
|
|
218
|
+
local propData = ReflectionService:GetPropertiesOfClass(instance.ClassName)
|
|
219
|
+
for _, prop in propData do
|
|
220
|
+
if not IGNORED_PROPERTIES[prop.Name] and not shouldSkip(prop.Name, "") then
|
|
221
|
+
local valueOk, value = pcall(function()
|
|
222
|
+
return instance[prop.Name]
|
|
223
|
+
end)
|
|
224
|
+
if valueOk then
|
|
225
|
+
local valueStr = tostring(value)
|
|
226
|
+
if not shouldSkip(prop.Name, valueStr) and not isBoringDefault(prop.Name, valueStr) then
|
|
227
|
+
table.insert(info, {
|
|
228
|
+
name = prop.Name,
|
|
229
|
+
value = valueStr,
|
|
230
|
+
type = prop.Type and tostring(prop.Type) or typeof(value),
|
|
231
|
+
category = prop.Display and prop.Display.Category or "Other",
|
|
232
|
+
})
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
return info
|
|
238
|
+
end)
|
|
239
|
+
|
|
240
|
+
if ok then
|
|
241
|
+
properties = apiData
|
|
242
|
+
else
|
|
243
|
+
local commonProps =
|
|
244
|
+
{ "Name", "Position", "Size", "Color", "Material", "Transparency", "Anchored", "CanCollide" }
|
|
245
|
+
for _, propName in commonProps do
|
|
246
|
+
local valOk, val = pcall(function()
|
|
247
|
+
return instance[propName]
|
|
248
|
+
end)
|
|
249
|
+
if valOk and val ~= nil then
|
|
250
|
+
table.insert(properties, {
|
|
251
|
+
name = propName,
|
|
252
|
+
value = tostring(val),
|
|
253
|
+
type = typeof(val),
|
|
254
|
+
})
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
return properties
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
local results = {}
|
|
263
|
+
|
|
264
|
+
-- Include the root instance itself
|
|
265
|
+
table.insert(results, {
|
|
266
|
+
name = rootInstance.Name,
|
|
267
|
+
className = rootInstance.ClassName,
|
|
268
|
+
path = Explorer.getPath(rootInstance),
|
|
269
|
+
properties = getProps(rootInstance),
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
for _, descendant in rootInstance:GetDescendants() do
|
|
273
|
+
table.insert(results, {
|
|
274
|
+
name = descendant.Name,
|
|
275
|
+
className = descendant.ClassName,
|
|
276
|
+
path = Explorer.getPath(descendant),
|
|
277
|
+
properties = getProps(descendant),
|
|
278
|
+
})
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
return { success = true, descendants = results }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
--[[
|
|
285
|
+
Smart type coercion: converts JSON-friendly values into Roblox types.
|
|
286
|
+
Uses the expected property type from ReflectionService when available.
|
|
287
|
+
]]
|
|
288
|
+
local function coerceValue(instance, key, value)
|
|
289
|
+
local expectedType = nil
|
|
290
|
+
local typeMap = getPropertyTypeMap(instance.ClassName)
|
|
291
|
+
expectedType = typeMap[key]
|
|
292
|
+
|
|
293
|
+
-- If we don't know the expected type, try reading the current value
|
|
294
|
+
if not expectedType then
|
|
295
|
+
local curOk, curVal = pcall(function()
|
|
296
|
+
return instance[key]
|
|
297
|
+
end)
|
|
298
|
+
if curOk and curVal ~= nil then
|
|
299
|
+
expectedType = typeof(curVal)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
-- ═══════════════════════════════════════════
|
|
304
|
+
-- UDim2 (GUI Size, Position, etc.)
|
|
305
|
+
-- ═══════════════════════════════════════════
|
|
306
|
+
if expectedType == "UDim2" then
|
|
307
|
+
if type(value) == "table" then
|
|
308
|
+
-- {xScale, xOffset, yScale, yOffset} or named keys
|
|
309
|
+
if value.XScale or value.xScale or value.X then
|
|
310
|
+
return UDim2.new(
|
|
311
|
+
value.XScale or value.xScale or value.X and value.X.Scale or 0,
|
|
312
|
+
value.XOffset or value.xOffset or value.X and value.X.Offset or 0,
|
|
313
|
+
value.YScale or value.yScale or value.Y and value.Y.Scale or 0,
|
|
314
|
+
value.YOffset or value.yOffset or value.Y and value.Y.Offset or 0
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
-- {X = {Scale, Offset}, Y = {Scale, Offset}}
|
|
318
|
+
if value.X and type(value.X) == "table" then
|
|
319
|
+
return UDim2.new(
|
|
320
|
+
value.X.Scale or value.X[1] or 0,
|
|
321
|
+
value.X.Offset or value.X[2] or 0,
|
|
322
|
+
value.Y and (value.Y.Scale or value.Y[1]) or 0,
|
|
323
|
+
value.Y and (value.Y.Offset or value.Y[2]) or 0
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
-- Array: {xScale, xOffset, yScale, yOffset}
|
|
327
|
+
if value[1] ~= nil then
|
|
328
|
+
return UDim2.new(value[1] or 0, value[2] or 0, value[3] or 0, value[4] or 0)
|
|
329
|
+
end
|
|
330
|
+
-- fromScale shorthand: {scale_x, scale_y} when only 2 elements
|
|
331
|
+
return UDim2.new(0, 0, 0, 0)
|
|
332
|
+
end
|
|
333
|
+
if type(value) == "string" then
|
|
334
|
+
-- "0.5, 0, 0.5, 0" format
|
|
335
|
+
local a, b, c, d = value:match("([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)")
|
|
336
|
+
if a then
|
|
337
|
+
return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d))
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
-- ═══════════════════════════════════════════
|
|
343
|
+
-- UDim
|
|
344
|
+
-- ═══════════════════════════════════════════
|
|
345
|
+
if expectedType == "UDim" then
|
|
346
|
+
if type(value) == "table" then
|
|
347
|
+
return UDim.new(value.Scale or value[1] or 0, value.Offset or value[2] or 0)
|
|
348
|
+
end
|
|
349
|
+
if type(value) == "number" then
|
|
350
|
+
return UDim.new(value, 0)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
-- ═══════════════════════════════════════════
|
|
355
|
+
-- Vector3
|
|
356
|
+
-- ═══════════════════════════════════════════
|
|
357
|
+
if expectedType == "Vector3" then
|
|
358
|
+
if type(value) == "table" then
|
|
359
|
+
return Vector3.new(
|
|
360
|
+
value.X or value.x or value[1] or 0,
|
|
361
|
+
value.Y or value.y or value[2] or 0,
|
|
362
|
+
value.Z or value.z or value[3] or 0
|
|
363
|
+
)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
-- ═══════════════════════════════════════════
|
|
368
|
+
-- Vector2
|
|
369
|
+
-- ═══════════════════════════════════════════
|
|
370
|
+
if expectedType == "Vector2" then
|
|
371
|
+
if type(value) == "table" then
|
|
372
|
+
return Vector2.new(value.X or value.x or value[1] or 0, value.Y or value.y or value[2] or 0)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
-- ═══════════════════════════════════════════
|
|
377
|
+
-- Color3
|
|
378
|
+
-- ═══════════════════════════════════════════
|
|
379
|
+
if expectedType == "Color3" then
|
|
380
|
+
if type(value) == "string" then
|
|
381
|
+
if value:sub(1, 1) == "#" then
|
|
382
|
+
return Color3.fromHex(value)
|
|
383
|
+
end
|
|
384
|
+
if value:match("^rgb") then
|
|
385
|
+
local r, g, b = value:match("(%d+)%D+(%d+)%D+(%d+)")
|
|
386
|
+
if r then
|
|
387
|
+
return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
local ok, bc = pcall(BrickColor.new, value)
|
|
391
|
+
if ok then
|
|
392
|
+
return bc.Color
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
if type(value) == "table" then
|
|
396
|
+
-- {R, G, B} 0-1 or {r, g, b}
|
|
397
|
+
if value.R or value.r then
|
|
398
|
+
return Color3.new(value.R or value.r or 0, value.G or value.g or 0, value.B or value.b or 0)
|
|
399
|
+
end
|
|
400
|
+
-- {red, green, blue} 0-255
|
|
401
|
+
if value.red or value.Red then
|
|
402
|
+
return Color3.fromRGB(
|
|
403
|
+
value.red or value.Red or 0,
|
|
404
|
+
value.green or value.Green or 0,
|
|
405
|
+
value.blue or value.Blue or 0
|
|
406
|
+
)
|
|
407
|
+
end
|
|
408
|
+
-- Array [r, g, b]
|
|
409
|
+
if value[1] then
|
|
410
|
+
if value[1] > 1 or value[2] > 1 or value[3] > 1 then
|
|
411
|
+
return Color3.fromRGB(value[1], value[2], value[3])
|
|
412
|
+
end
|
|
413
|
+
return Color3.new(value[1], value[2], value[3])
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
-- ═══════════════════════════════════════════
|
|
419
|
+
-- BrickColor
|
|
420
|
+
-- ═══════════════════════════════════════════
|
|
421
|
+
if expectedType == "BrickColor" then
|
|
422
|
+
if type(value) == "string" then
|
|
423
|
+
return BrickColor.new(value)
|
|
424
|
+
end
|
|
425
|
+
if type(value) == "number" then
|
|
426
|
+
return BrickColor.new(value)
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
-- ═══════════════════════════════════════════
|
|
431
|
+
-- CFrame
|
|
432
|
+
-- ═══════════════════════════════════════════
|
|
433
|
+
if expectedType == "CFrame" then
|
|
434
|
+
if type(value) == "table" then
|
|
435
|
+
if value.Position or value.position then
|
|
436
|
+
local pos = value.Position or value.position
|
|
437
|
+
local cf = CFrame.new(
|
|
438
|
+
pos.x or pos.X or pos[1] or 0,
|
|
439
|
+
pos.y or pos.Y or pos[2] or 0,
|
|
440
|
+
pos.z or pos.Z or pos[3] or 0
|
|
441
|
+
)
|
|
442
|
+
if value.Rotation or value.rotation then
|
|
443
|
+
local rot = value.Rotation or value.rotation
|
|
444
|
+
cf = cf
|
|
445
|
+
* CFrame.Angles(
|
|
446
|
+
math.rad(rot.x or rot.X or rot[1] or 0),
|
|
447
|
+
math.rad(rot.y or rot.Y or rot[2] or 0),
|
|
448
|
+
math.rad(rot.z or rot.Z or rot[3] or 0)
|
|
449
|
+
)
|
|
450
|
+
end
|
|
451
|
+
return cf
|
|
452
|
+
end
|
|
453
|
+
-- Simple {x, y, z}
|
|
454
|
+
if value[1] then
|
|
455
|
+
return CFrame.new(value[1], value[2] or 0, value[3] or 0)
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
-- ═══════════════════════════════════════════
|
|
461
|
+
-- Rect (for GUI ImageRectOffset, etc.)
|
|
462
|
+
-- ═══════════════════════════════════════════
|
|
463
|
+
if expectedType == "Rect" then
|
|
464
|
+
if type(value) == "table" then
|
|
465
|
+
return Rect.new(
|
|
466
|
+
value[1] or value.Min and value.Min[1] or 0,
|
|
467
|
+
value[2] or value.Min and value.Min[2] or 0,
|
|
468
|
+
value[3] or value.Max and value.Max[1] or 0,
|
|
469
|
+
value[4] or value.Max and value.Max[2] or 0
|
|
470
|
+
)
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
-- ═══════════════════════════════════════════
|
|
475
|
+
-- NumberRange
|
|
476
|
+
-- ═══════════════════════════════════════════
|
|
477
|
+
if expectedType == "NumberRange" then
|
|
478
|
+
if type(value) == "table" then
|
|
479
|
+
return NumberRange.new(value.Min or value[1] or 0, value.Max or value[2] or 1)
|
|
480
|
+
end
|
|
481
|
+
if type(value) == "number" then
|
|
482
|
+
return NumberRange.new(value)
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
-- ═══════════════════════════════════════════
|
|
487
|
+
-- NumberSequence (for transparency gradients, etc.)
|
|
488
|
+
-- ═══════════════════════════════════════════
|
|
489
|
+
if expectedType == "NumberSequence" then
|
|
490
|
+
if type(value) == "table" then
|
|
491
|
+
if value[1] and type(value[1]) == "table" then
|
|
492
|
+
local keypoints = {}
|
|
493
|
+
for _, kp in value do
|
|
494
|
+
table.insert(
|
|
495
|
+
keypoints,
|
|
496
|
+
NumberSequenceKeypoint.new(
|
|
497
|
+
kp.Time or kp[1] or 0,
|
|
498
|
+
kp.Value or kp[2] or 0,
|
|
499
|
+
kp.Envelope or kp[3] or 0
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
end
|
|
503
|
+
return NumberSequence.new(keypoints)
|
|
504
|
+
end
|
|
505
|
+
if #value == 2 and type(value[1]) == "number" then
|
|
506
|
+
return NumberSequence.new(value[1], value[2])
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
if type(value) == "number" then
|
|
510
|
+
return NumberSequence.new(value)
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
-- ═══════════════════════════════════════════
|
|
515
|
+
-- ColorSequence
|
|
516
|
+
-- ═══════════════════════════════════════════
|
|
517
|
+
if expectedType == "ColorSequence" then
|
|
518
|
+
if type(value) == "table" then
|
|
519
|
+
if value[1] and type(value[1]) == "table" then
|
|
520
|
+
local keypoints = {}
|
|
521
|
+
for _, kp in value do
|
|
522
|
+
local color = Color3.new(1, 1, 1)
|
|
523
|
+
if kp.Color then
|
|
524
|
+
if type(kp.Color) == "string" and kp.Color:sub(1, 1) == "#" then
|
|
525
|
+
color = Color3.fromHex(kp.Color)
|
|
526
|
+
elseif type(kp.Color) == "table" then
|
|
527
|
+
color = Color3.new(kp.Color[1] or 0, kp.Color[2] or 0, kp.Color[3] or 0)
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
table.insert(keypoints, ColorSequenceKeypoint.new(kp.Time or kp[1] or 0, color))
|
|
531
|
+
end
|
|
532
|
+
return ColorSequence.new(keypoints)
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
-- ═══════════════════════════════════════════
|
|
538
|
+
-- Font
|
|
539
|
+
-- ═══════════════════════════════════════════
|
|
540
|
+
if expectedType == "Font" then
|
|
541
|
+
if type(value) == "table" then
|
|
542
|
+
local family = value.Family or value.family or "rbxasset://fonts/families/SourceSansPro.json"
|
|
543
|
+
local weight = Enum.FontWeight.Regular
|
|
544
|
+
local style = Enum.FontStyle.Normal
|
|
545
|
+
if value.Weight or value.weight then
|
|
546
|
+
pcall(function()
|
|
547
|
+
weight = Enum.FontWeight[value.Weight or value.weight]
|
|
548
|
+
end)
|
|
549
|
+
end
|
|
550
|
+
if value.Style or value.style then
|
|
551
|
+
pcall(function()
|
|
552
|
+
style = Enum.FontStyle[value.Style or value.style]
|
|
553
|
+
end)
|
|
554
|
+
end
|
|
555
|
+
return Font.new(family, weight, style)
|
|
556
|
+
end
|
|
557
|
+
if type(value) == "string" then
|
|
558
|
+
return Font.new(value)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
-- ═══════════════════════════════════════════
|
|
563
|
+
-- EnumItem (any enum property)
|
|
564
|
+
-- ═══════════════════════════════════════════
|
|
565
|
+
if type(value) == "string" then
|
|
566
|
+
local currentVal = nil
|
|
567
|
+
pcall(function()
|
|
568
|
+
currentVal = instance[key]
|
|
569
|
+
end)
|
|
570
|
+
if typeof(currentVal) == "EnumItem" then
|
|
571
|
+
local enumType = tostring(currentVal.EnumType)
|
|
572
|
+
local enumOk, enumVal = pcall(function()
|
|
573
|
+
return Enum[enumType][value]
|
|
574
|
+
end)
|
|
575
|
+
if enumOk then
|
|
576
|
+
return enumVal
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
-- ═══════════════════════════════════════════
|
|
582
|
+
-- Fallback: type-agnostic guesses (no expectedType known)
|
|
583
|
+
-- ═══════════════════════════════════════════
|
|
584
|
+
if type(value) == "table" and not expectedType then
|
|
585
|
+
-- UDim2 guess: has XScale/YScale keys
|
|
586
|
+
if value.XScale or value.xScale then
|
|
587
|
+
return UDim2.new(
|
|
588
|
+
value.XScale or value.xScale or 0,
|
|
589
|
+
value.XOffset or value.xOffset or 0,
|
|
590
|
+
value.YScale or value.yScale or 0,
|
|
591
|
+
value.YOffset or value.yOffset or 0
|
|
592
|
+
)
|
|
593
|
+
end
|
|
594
|
+
-- Vector3 guess: 3 number elements or X/Y/Z keys
|
|
595
|
+
if (value.X or value.x) and (value.Y or value.y) and (value.Z or value.z) then
|
|
596
|
+
return Vector3.new(value.X or value.x, value.Y or value.y, value.Z or value.z)
|
|
597
|
+
end
|
|
598
|
+
if value[1] and value[2] and value[3] and not value[4] then
|
|
599
|
+
return Vector3.new(value[1], value[2], value[3])
|
|
600
|
+
end
|
|
601
|
+
-- Color3 guess: R/G/B keys
|
|
602
|
+
if value.R or value.r then
|
|
603
|
+
return Color3.new(value.R or value.r or 0, value.G or value.g or 0, value.B or value.b or 0)
|
|
604
|
+
end
|
|
605
|
+
-- Vector2 guess: 2 number elements
|
|
606
|
+
if value[1] and value[2] and not value[3] then
|
|
607
|
+
return Vector2.new(value[1], value[2])
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
-- String-based color on color properties (fallback)
|
|
612
|
+
if type(value) == "string" and (key == "Color" or key == "Color3" or key:match("Color$")) then
|
|
613
|
+
if value:sub(1, 1) == "#" then
|
|
614
|
+
return Color3.fromHex(value)
|
|
615
|
+
end
|
|
616
|
+
local ok, bc = pcall(BrickColor.new, value)
|
|
617
|
+
if ok then
|
|
618
|
+
return bc.Color
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
if type(value) == "string" and key == "BrickColor" then
|
|
623
|
+
return BrickColor.new(value)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
return value
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
function Properties.set(path, propertiesToSet)
|
|
630
|
+
local instance = Explorer.resolveInstance(path)
|
|
631
|
+
if not instance then
|
|
632
|
+
return { success = false, error = "Instance not found: " .. path }
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
local errors = {}
|
|
636
|
+
local set = {}
|
|
637
|
+
|
|
638
|
+
for key, value in propertiesToSet do
|
|
639
|
+
local coerced = coerceValue(instance, key, value)
|
|
640
|
+
local ok, err = pcall(function()
|
|
641
|
+
instance[key] = coerced
|
|
642
|
+
end)
|
|
643
|
+
if ok then
|
|
644
|
+
table.insert(set, key)
|
|
645
|
+
else
|
|
646
|
+
table.insert(errors, key .. ": " .. tostring(err))
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
if #errors > 0 then
|
|
651
|
+
return { success = false, error = table.concat(errors, "; "), set = set }
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
return { success = true, set = set }
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
function Properties.bulkSet(spec)
|
|
658
|
+
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
659
|
+
local recording = ChangeHistoryService:TryBeginRecording("Dominus: Bulk set properties")
|
|
660
|
+
|
|
661
|
+
local modified = 0
|
|
662
|
+
local errors = {}
|
|
663
|
+
|
|
664
|
+
if spec.targets and type(spec.targets) == "table" then
|
|
665
|
+
-- Explicit mode: array of {path, properties}
|
|
666
|
+
for _, target in ipairs(spec.targets) do
|
|
667
|
+
local instance = Explorer.resolveInstance(target.path)
|
|
668
|
+
if not instance then
|
|
669
|
+
table.insert(errors, "Not found: " .. tostring(target.path))
|
|
670
|
+
else
|
|
671
|
+
for key, value in target.properties do
|
|
672
|
+
local coerced = coerceValue(instance, key, value)
|
|
673
|
+
local ok, err = pcall(function()
|
|
674
|
+
instance[key] = coerced
|
|
675
|
+
end)
|
|
676
|
+
if not ok then
|
|
677
|
+
table.insert(errors, target.path .. "." .. key .. ": " .. tostring(err))
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
modified = modified + 1
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
elseif spec.root and (spec.properties or spec.addChildren) then
|
|
684
|
+
-- Filter mode: apply to all matching descendants
|
|
685
|
+
local root = Explorer.resolveInstance(spec.root)
|
|
686
|
+
if not root then
|
|
687
|
+
if recording then
|
|
688
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
689
|
+
end
|
|
690
|
+
return { success = false, error = "Root not found: " .. tostring(spec.root) }
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
local function processInstance(inst)
|
|
694
|
+
if spec.className and inst.ClassName ~= spec.className then
|
|
695
|
+
return
|
|
696
|
+
end
|
|
697
|
+
if spec.properties then
|
|
698
|
+
for key, value in spec.properties do
|
|
699
|
+
local coerced = coerceValue(inst, key, value)
|
|
700
|
+
local ok, err = pcall(function()
|
|
701
|
+
inst[key] = coerced
|
|
702
|
+
end)
|
|
703
|
+
if not ok then
|
|
704
|
+
table.insert(errors, Explorer.getPath(inst) .. "." .. key .. ": " .. tostring(err))
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
-- addChildren: insert new child instances (e.g. UIStroke, UICorner)
|
|
709
|
+
if spec.addChildren and type(spec.addChildren) == "table" then
|
|
710
|
+
for _, childSpec in ipairs(spec.addChildren) do
|
|
711
|
+
local childClass = childSpec.ClassName or childSpec.className
|
|
712
|
+
if childClass then
|
|
713
|
+
-- Skip if child of this class already exists (idempotent)
|
|
714
|
+
local skipIfExists = childSpec.skipIfExists ~= false
|
|
715
|
+
if skipIfExists then
|
|
716
|
+
local existing = inst:FindFirstChildOfClass(childClass)
|
|
717
|
+
if existing then
|
|
718
|
+
continue
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
local ok2, child = pcall(Instance.new, childClass)
|
|
722
|
+
if ok2 and child then
|
|
723
|
+
if childSpec.Name or childSpec.name then
|
|
724
|
+
child.Name = childSpec.Name or childSpec.name
|
|
725
|
+
end
|
|
726
|
+
-- Set props on the new child
|
|
727
|
+
local props = childSpec.properties
|
|
728
|
+
or childSpec.Properties
|
|
729
|
+
or childSpec.props
|
|
730
|
+
or childSpec.Props
|
|
731
|
+
or {}
|
|
732
|
+
-- Also check flattened keys
|
|
733
|
+
for k, v in childSpec do
|
|
734
|
+
if
|
|
735
|
+
k ~= "ClassName"
|
|
736
|
+
and k ~= "className"
|
|
737
|
+
and k ~= "Name"
|
|
738
|
+
and k ~= "name"
|
|
739
|
+
and k ~= "properties"
|
|
740
|
+
and k ~= "Properties"
|
|
741
|
+
and k ~= "props"
|
|
742
|
+
and k ~= "Props"
|
|
743
|
+
and k ~= "skipIfExists"
|
|
744
|
+
and k ~= "Children"
|
|
745
|
+
and k ~= "children"
|
|
746
|
+
then
|
|
747
|
+
props[k] = v
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
for pk, pv in props do
|
|
751
|
+
local coerced = coerceValue(child, pk, pv)
|
|
752
|
+
pcall(function()
|
|
753
|
+
child[pk] = coerced
|
|
754
|
+
end)
|
|
755
|
+
end
|
|
756
|
+
child.Parent = inst
|
|
757
|
+
else
|
|
758
|
+
table.insert(
|
|
759
|
+
errors,
|
|
760
|
+
Explorer.getPath(inst) .. ": failed to create " .. tostring(childClass)
|
|
761
|
+
)
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
modified = modified + 1
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
-- Include root itself if it matches
|
|
770
|
+
processInstance(root)
|
|
771
|
+
for _, desc in root:GetDescendants() do
|
|
772
|
+
processInstance(desc)
|
|
773
|
+
end
|
|
774
|
+
else
|
|
775
|
+
if recording then
|
|
776
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
777
|
+
end
|
|
778
|
+
return { success = false, error = "Provide either 'targets' array or 'root' + 'properties'/'addChildren'" }
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
if recording then
|
|
782
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
return { success = true, modified = modified, errors = errors }
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
function Properties.inspectV2(spec)
|
|
789
|
+
if type(spec.targets) ~= "table" or #spec.targets == 0 then
|
|
790
|
+
return { success = false, error = "targets must contain at least one instance reference" }
|
|
791
|
+
end
|
|
792
|
+
if #spec.targets > 20 then
|
|
793
|
+
return { success = false, error = "At most 20 instances can be inspected at once" }
|
|
794
|
+
end
|
|
795
|
+
local compact = spec.compact ~= false
|
|
796
|
+
local results = {}
|
|
797
|
+
|
|
798
|
+
for index, targetRef in spec.targets do
|
|
799
|
+
local instance, err = InstanceRegistry.resolve(targetRef)
|
|
800
|
+
if not instance then
|
|
801
|
+
table.insert(results, { success = false, index = index, error = err })
|
|
802
|
+
continue
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
local metadata = Reflection.getPropertyMetadata(instance.ClassName)
|
|
806
|
+
local propertyNames = {}
|
|
807
|
+
for propertyName in metadata do
|
|
808
|
+
if not IGNORED_PROPERTIES[propertyName] and (not compact or not COMPACT_SKIP[propertyName]) then
|
|
809
|
+
table.insert(propertyNames, propertyName)
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
table.sort(propertyNames)
|
|
813
|
+
|
|
814
|
+
local properties = {}
|
|
815
|
+
for _, propertyName in propertyNames do
|
|
816
|
+
local readOk, value = pcall(function()
|
|
817
|
+
return instance[propertyName]
|
|
818
|
+
end)
|
|
819
|
+
if readOk and value ~= nil then
|
|
820
|
+
table.insert(properties, {
|
|
821
|
+
name = propertyName,
|
|
822
|
+
type = metadata[propertyName].type,
|
|
823
|
+
category = metadata[propertyName].category,
|
|
824
|
+
writable = metadata[propertyName].writable,
|
|
825
|
+
value = ValueCodec.encode(value),
|
|
826
|
+
})
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
local children = instance:GetChildren()
|
|
831
|
+
table.sort(children, function(a, b)
|
|
832
|
+
if a.Name == b.Name then
|
|
833
|
+
return a.ClassName < b.ClassName
|
|
834
|
+
end
|
|
835
|
+
return a.Name < b.Name
|
|
836
|
+
end)
|
|
837
|
+
local childRefs = {}
|
|
838
|
+
for _, child in children do
|
|
839
|
+
table.insert(childRefs, {
|
|
840
|
+
name = child.Name,
|
|
841
|
+
className = child.ClassName,
|
|
842
|
+
ref = InstanceRegistry.toRef(child),
|
|
843
|
+
})
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
table.insert(results, {
|
|
847
|
+
success = true,
|
|
848
|
+
name = instance.Name,
|
|
849
|
+
className = instance.ClassName,
|
|
850
|
+
ref = InstanceRegistry.toRef(instance),
|
|
851
|
+
properties = properties,
|
|
852
|
+
children = childRefs,
|
|
853
|
+
})
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
return { success = true, results = results }
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
return Properties
|