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
package/plugin/src/UIBuilder.lua
CHANGED
|
@@ -1,727 +1,857 @@
|
|
|
1
|
-
--[[
|
|
2
|
-
UIBuilder: create entire UI trees from a declarative JSON spec in one call.
|
|
3
|
-
Handles all type coercion natively in Luau — no round trips needed.
|
|
4
|
-
]]
|
|
5
|
-
|
|
6
|
-
local Explorer = require(script.Parent.Explorer)
|
|
7
|
-
local
|
|
8
|
-
|
|
9
|
-
local
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
local
|
|
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
|
-
return Color3.new(value
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
value.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
value.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if type(value) == "
|
|
84
|
-
return UDim.new(0, value)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
end
|
|
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
|
-
|
|
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
|
-
end
|
|
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
|
-
if type(value) == "
|
|
222
|
-
return NumberRange.new(value)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
value.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if
|
|
249
|
-
return
|
|
250
|
-
end
|
|
251
|
-
if
|
|
252
|
-
return
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if t == "
|
|
264
|
-
return
|
|
265
|
-
end
|
|
266
|
-
if t == "
|
|
267
|
-
return
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
pcall(function()
|
|
366
|
-
|
|
367
|
-
end)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
local
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
--
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
local
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if t == "
|
|
564
|
-
return { value.Scale, value.Offset }
|
|
565
|
-
end
|
|
566
|
-
if t == "
|
|
567
|
-
return { value.
|
|
568
|
-
end
|
|
569
|
-
if t == "
|
|
570
|
-
return { value.X, value.Y
|
|
571
|
-
end
|
|
572
|
-
if t == "
|
|
573
|
-
return
|
|
574
|
-
end
|
|
575
|
-
if t == "
|
|
576
|
-
return value
|
|
577
|
-
end
|
|
578
|
-
if t == "
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return
|
|
594
|
-
end
|
|
595
|
-
if t == "
|
|
596
|
-
return { value.Min
|
|
597
|
-
end
|
|
598
|
-
if t == "
|
|
599
|
-
return { value
|
|
600
|
-
end
|
|
601
|
-
if t == "
|
|
602
|
-
return value
|
|
603
|
-
end
|
|
604
|
-
if t == "
|
|
605
|
-
return
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
end
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
end
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
end
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
end
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
end
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
end
|
|
726
|
-
|
|
727
|
-
return
|
|
1
|
+
--[[
|
|
2
|
+
UIBuilder: create entire UI trees from a declarative JSON spec in one call.
|
|
3
|
+
Handles all type coercion natively in Luau — no round trips needed.
|
|
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 ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
11
|
+
|
|
12
|
+
local UIBuilder = {}
|
|
13
|
+
|
|
14
|
+
-- Cache property type maps per ClassName to avoid repeated reflection calls
|
|
15
|
+
local propertyTypeCache = {}
|
|
16
|
+
local function getPropertyType(className, propName)
|
|
17
|
+
if not propertyTypeCache[className] then
|
|
18
|
+
propertyTypeCache[className] = Reflection.getPropertyTypes(className)
|
|
19
|
+
end
|
|
20
|
+
return propertyTypeCache[className][propName]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
local function resolveColor(value)
|
|
24
|
+
if type(value) == "string" then
|
|
25
|
+
if value:sub(1, 1) == "#" then
|
|
26
|
+
return Color3.fromHex(value)
|
|
27
|
+
end
|
|
28
|
+
local r, g, b = value:match("rgb%s*%((%d+)%D+(%d+)%D+(%d+)%)")
|
|
29
|
+
if r then
|
|
30
|
+
return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))
|
|
31
|
+
end
|
|
32
|
+
local ok, bc = pcall(BrickColor.new, value)
|
|
33
|
+
if ok then
|
|
34
|
+
return bc.Color
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
if type(value) == "table" then
|
|
38
|
+
if value[1] then
|
|
39
|
+
if value[1] > 1 or value[2] > 1 or value[3] > 1 then
|
|
40
|
+
return Color3.fromRGB(value[1], value[2], value[3])
|
|
41
|
+
end
|
|
42
|
+
return Color3.new(value[1], value[2], value[3])
|
|
43
|
+
end
|
|
44
|
+
if value.R or value.r then
|
|
45
|
+
return Color3.new(value.R or value.r or 0, value.G or value.g or 0, value.B or value.b or 0)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
return Color3.new(1, 1, 1)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
local function resolveUDim2(value)
|
|
52
|
+
if type(value) == "table" then
|
|
53
|
+
if value.XScale or value.xScale then
|
|
54
|
+
return UDim2.new(
|
|
55
|
+
value.XScale or value.xScale or 0,
|
|
56
|
+
value.XOffset or value.xOffset or 0,
|
|
57
|
+
value.YScale or value.yScale or 0,
|
|
58
|
+
value.YOffset or value.yOffset or 0
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
if value.X and type(value.X) == "table" then
|
|
62
|
+
return UDim2.new(
|
|
63
|
+
value.X.Scale or value.X[1] or 0,
|
|
64
|
+
value.X.Offset or value.X[2] or 0,
|
|
65
|
+
value.Y and (value.Y.Scale or value.Y[1]) or 0,
|
|
66
|
+
value.Y and (value.Y.Offset or value.Y[2]) or 0
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
if value[1] ~= nil then
|
|
70
|
+
return UDim2.new(value[1] or 0, value[2] or 0, value[3] or 0, value[4] or 0)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
if type(value) == "string" then
|
|
74
|
+
local a, b, c, d = value:match("([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)")
|
|
75
|
+
if a then
|
|
76
|
+
return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
return UDim2.new(0, 0, 0, 0)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
local function resolveUDim(value)
|
|
83
|
+
if type(value) == "table" then
|
|
84
|
+
return UDim.new(value.Scale or value[1] or 0, value.Offset or value[2] or 0)
|
|
85
|
+
end
|
|
86
|
+
if type(value) == "number" then
|
|
87
|
+
return UDim.new(0, value)
|
|
88
|
+
end
|
|
89
|
+
return UDim.new(0, 0)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
local function resolveVector2(value)
|
|
93
|
+
if type(value) == "table" then
|
|
94
|
+
return Vector2.new(value.X or value.x or value[1] or 0, value.Y or value.y or value[2] or 0)
|
|
95
|
+
end
|
|
96
|
+
return Vector2.new(0, 0)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
local function resolveEnum(instance, key, value)
|
|
100
|
+
if type(value) ~= "string" then
|
|
101
|
+
return value
|
|
102
|
+
end
|
|
103
|
+
local curOk, curVal = pcall(function()
|
|
104
|
+
return instance[key]
|
|
105
|
+
end)
|
|
106
|
+
if curOk and typeof(curVal) == "EnumItem" then
|
|
107
|
+
local enumType = tostring(curVal.EnumType)
|
|
108
|
+
local ok, enumVal = pcall(function()
|
|
109
|
+
return Enum[enumType][value]
|
|
110
|
+
end)
|
|
111
|
+
if ok then
|
|
112
|
+
return enumVal
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
return value
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
local function resolveFont(value)
|
|
119
|
+
if type(value) == "string" then
|
|
120
|
+
local ok, font = pcall(function()
|
|
121
|
+
return Enum.Font[value]
|
|
122
|
+
end)
|
|
123
|
+
if ok then
|
|
124
|
+
return font
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
return value
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
local UDIM2_PROPS = {
|
|
131
|
+
Size = true,
|
|
132
|
+
Position = true,
|
|
133
|
+
CellSize = true,
|
|
134
|
+
CellPadding = true,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
local COLOR3_PROPS = {
|
|
138
|
+
BackgroundColor3 = true,
|
|
139
|
+
BorderColor3 = true,
|
|
140
|
+
TextColor3 = true,
|
|
141
|
+
ImageColor3 = true,
|
|
142
|
+
PlaceholderColor3 = true,
|
|
143
|
+
TextStrokeColor3 = true,
|
|
144
|
+
ScrollBarImageColor3 = true,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
local function resolveColorSequence(value)
|
|
148
|
+
if type(value) == "table" then
|
|
149
|
+
-- Array of keypoints: {{time, color}, ...}
|
|
150
|
+
if type(value[1]) == "table" then
|
|
151
|
+
local keypoints = {}
|
|
152
|
+
for _, kp in ipairs(value) do
|
|
153
|
+
local t = kp[1] or kp.Time or 0
|
|
154
|
+
local c = resolveColor(kp[2] or kp.Color or kp)
|
|
155
|
+
table.insert(keypoints, ColorSequenceKeypoint.new(t, c))
|
|
156
|
+
end
|
|
157
|
+
return ColorSequence.new(keypoints)
|
|
158
|
+
end
|
|
159
|
+
-- Single color as array [r,g,b] → uniform ColorSequence
|
|
160
|
+
if type(value[1]) == "number" and #value == 3 then
|
|
161
|
+
local c = resolveColor(value)
|
|
162
|
+
return ColorSequence.new(c)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
-- String hex/rgb → uniform ColorSequence
|
|
166
|
+
if type(value) == "string" then
|
|
167
|
+
return ColorSequence.new(resolveColor(value))
|
|
168
|
+
end
|
|
169
|
+
return ColorSequence.new(Color3.new(1, 1, 1))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
local function resolveNumberSequence(value)
|
|
173
|
+
if type(value) == "table" then
|
|
174
|
+
-- Array of keypoints: {{time, value, envelope?}, ...}
|
|
175
|
+
if type(value[1]) == "table" then
|
|
176
|
+
local keypoints = {}
|
|
177
|
+
for _, kp in ipairs(value) do
|
|
178
|
+
local t = kp[1] or kp.Time or 0
|
|
179
|
+
local v = kp[2] or kp.Value or 0
|
|
180
|
+
local e = kp[3] or kp.Envelope or 0
|
|
181
|
+
table.insert(keypoints, NumberSequenceKeypoint.new(t, v, e))
|
|
182
|
+
end
|
|
183
|
+
return NumberSequence.new(keypoints)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
if type(value) == "number" then
|
|
187
|
+
return NumberSequence.new(value)
|
|
188
|
+
end
|
|
189
|
+
return NumberSequence.new(0)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
local VECTOR2_PROPS = {
|
|
193
|
+
AnchorPoint = true,
|
|
194
|
+
CanvasSize_v2 = true,
|
|
195
|
+
AbsoluteSize = true,
|
|
196
|
+
AbsolutePosition = true,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
local UDIM_PROPS = {
|
|
200
|
+
CornerRadius = true,
|
|
201
|
+
Padding = true,
|
|
202
|
+
PaddingTop = true,
|
|
203
|
+
PaddingBottom = true,
|
|
204
|
+
PaddingLeft = true,
|
|
205
|
+
PaddingRight = true,
|
|
206
|
+
FillDirection = false, -- not UDim
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
local NUMBER_RANGE_PROPS = {
|
|
210
|
+
Range = true,
|
|
211
|
+
RotationRange = true,
|
|
212
|
+
Size_NR = true, -- particle size range alias
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
local RECT_PROPS = {
|
|
216
|
+
SliceCenter = true,
|
|
217
|
+
ImageRectOffset_Rect = true,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
local function resolveNumberRange(value)
|
|
221
|
+
if type(value) == "table" then
|
|
222
|
+
return NumberRange.new(value[1] or 0, value[2] or value[1] or 0)
|
|
223
|
+
end
|
|
224
|
+
if type(value) == "number" then
|
|
225
|
+
return NumberRange.new(value)
|
|
226
|
+
end
|
|
227
|
+
return NumberRange.new(0)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
local function resolveRect(value)
|
|
231
|
+
if type(value) == "table" then
|
|
232
|
+
if #value == 4 then
|
|
233
|
+
return Rect.new(value[1], value[2], value[3], value[4])
|
|
234
|
+
end
|
|
235
|
+
return Rect.new(
|
|
236
|
+
value.Min and value.Min[1] or value.MinX or 0,
|
|
237
|
+
value.Min and value.Min[2] or value.MinY or 0,
|
|
238
|
+
value.Max and value.Max[1] or value.MaxX or 0,
|
|
239
|
+
value.Max and value.Max[2] or value.MaxY or 0
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
return Rect.new(0, 0, 0, 0)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
-- Disambiguate 2-element arrays: check actual property type on the instance
|
|
246
|
+
local function resolve2ElementArray(instance, key, value)
|
|
247
|
+
-- First, check our known lookup tables
|
|
248
|
+
if VECTOR2_PROPS[key] then
|
|
249
|
+
return resolveVector2(value)
|
|
250
|
+
end
|
|
251
|
+
if UDIM_PROPS[key] then
|
|
252
|
+
return resolveUDim(value)
|
|
253
|
+
end
|
|
254
|
+
if NUMBER_RANGE_PROPS[key] then
|
|
255
|
+
return resolveNumberRange(value)
|
|
256
|
+
end
|
|
257
|
+
-- Fall back to runtime type introspection
|
|
258
|
+
local ok, curVal = pcall(function()
|
|
259
|
+
return instance[key]
|
|
260
|
+
end)
|
|
261
|
+
if ok and curVal ~= nil then
|
|
262
|
+
local t = typeof(curVal)
|
|
263
|
+
if t == "Vector2" then
|
|
264
|
+
return resolveVector2(value)
|
|
265
|
+
end
|
|
266
|
+
if t == "UDim" then
|
|
267
|
+
return resolveUDim(value)
|
|
268
|
+
end
|
|
269
|
+
if t == "NumberRange" then
|
|
270
|
+
return resolveNumberRange(value)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
-- Default: assume UDim for 2-element (more common in UI)
|
|
274
|
+
return resolveUDim(value)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
local function applyProperty(instance, key, value)
|
|
278
|
+
-- Special handling by property name
|
|
279
|
+
if key == "Size" or key == "Position" then
|
|
280
|
+
if instance:IsA("GuiObject") or instance:IsA("UIBase") then
|
|
281
|
+
instance[key] = resolveUDim2(value)
|
|
282
|
+
return
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
if key == "AnchorPoint" then
|
|
287
|
+
if instance:IsA("GuiObject") then
|
|
288
|
+
instance[key] = resolveVector2(value)
|
|
289
|
+
return
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
if key == "CanvasSize" and instance:IsA("ScrollingFrame") then
|
|
294
|
+
instance[key] = resolveUDim2(value)
|
|
295
|
+
return
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
if UDIM_PROPS[key] then
|
|
299
|
+
instance[key] = resolveUDim(value)
|
|
300
|
+
return
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
-- Use reflection to distinguish Color3 vs ColorSequence, NumberSequence, etc.
|
|
304
|
+
local propType = getPropertyType(instance.ClassName, key)
|
|
305
|
+
|
|
306
|
+
if propType == "ColorSequence" then
|
|
307
|
+
instance[key] = resolveColorSequence(value)
|
|
308
|
+
return
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
if propType == "NumberSequence" then
|
|
312
|
+
instance[key] = resolveNumberSequence(value)
|
|
313
|
+
return
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
if COLOR3_PROPS[key] or propType == "Color3" then
|
|
317
|
+
instance[key] = resolveColor(value)
|
|
318
|
+
return
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
-- Reflection-based coercion for types not in hardcoded tables
|
|
322
|
+
if propType == "UDim2" and type(value) == "table" then
|
|
323
|
+
instance[key] = resolveUDim2(value)
|
|
324
|
+
return
|
|
325
|
+
end
|
|
326
|
+
if propType == "UDim" then
|
|
327
|
+
instance[key] = resolveUDim(value)
|
|
328
|
+
return
|
|
329
|
+
end
|
|
330
|
+
if propType == "Vector2" then
|
|
331
|
+
instance[key] = resolveVector2(value)
|
|
332
|
+
return
|
|
333
|
+
end
|
|
334
|
+
if propType == "NumberRange" then
|
|
335
|
+
instance[key] = resolveNumberRange(value)
|
|
336
|
+
return
|
|
337
|
+
end
|
|
338
|
+
if propType == "Rect" then
|
|
339
|
+
instance[key] = resolveRect(value)
|
|
340
|
+
return
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
-- Rect properties (4-element arrays like SliceCenter)
|
|
344
|
+
if RECT_PROPS[key] then
|
|
345
|
+
instance[key] = resolveRect(value)
|
|
346
|
+
return
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
-- NumberRange properties
|
|
350
|
+
if NUMBER_RANGE_PROPS[key] then
|
|
351
|
+
instance[key] = resolveNumberRange(value)
|
|
352
|
+
return
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
if key == "Font" and (instance:IsA("TextLabel") or instance:IsA("TextButton") or instance:IsA("TextBox")) then
|
|
356
|
+
instance[key] = resolveFont(value)
|
|
357
|
+
return
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
if key == "FontFace" then
|
|
361
|
+
if type(value) == "table" then
|
|
362
|
+
local family = value.Family or "rbxasset://fonts/families/SourceSansPro.json"
|
|
363
|
+
local weight = Enum.FontWeight.Regular
|
|
364
|
+
local style = Enum.FontStyle.Normal
|
|
365
|
+
pcall(function()
|
|
366
|
+
weight = Enum.FontWeight[value.Weight or "Regular"]
|
|
367
|
+
end)
|
|
368
|
+
pcall(function()
|
|
369
|
+
style = Enum.FontStyle[value.Style or "Normal"]
|
|
370
|
+
end)
|
|
371
|
+
instance.FontFace = Font.new(family, weight, style)
|
|
372
|
+
end
|
|
373
|
+
return
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
-- Enum properties (string values)
|
|
377
|
+
if type(value) == "string" then
|
|
378
|
+
local resolved = resolveEnum(instance, key, value)
|
|
379
|
+
local ok, err = pcall(function()
|
|
380
|
+
instance[key] = resolved
|
|
381
|
+
end)
|
|
382
|
+
if not ok then
|
|
383
|
+
-- Maybe it's a direct value
|
|
384
|
+
pcall(function()
|
|
385
|
+
instance[key] = value
|
|
386
|
+
end)
|
|
387
|
+
end
|
|
388
|
+
return
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
if UDIM2_PROPS[key] then
|
|
392
|
+
pcall(function()
|
|
393
|
+
instance[key] = resolveUDim2(value)
|
|
394
|
+
end)
|
|
395
|
+
return
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
-- Smart disambiguation for 2-element table arrays
|
|
399
|
+
if type(value) == "table" and #value == 2 then
|
|
400
|
+
local resolved = resolve2ElementArray(instance, key, value)
|
|
401
|
+
pcall(function()
|
|
402
|
+
instance[key] = resolved
|
|
403
|
+
end)
|
|
404
|
+
return
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
-- 4-element table arrays: try Rect if not already handled as UDim2
|
|
408
|
+
if type(value) == "table" and #value == 4 then
|
|
409
|
+
-- Check actual property type
|
|
410
|
+
local ok, curVal = pcall(function()
|
|
411
|
+
return instance[key]
|
|
412
|
+
end)
|
|
413
|
+
if ok and curVal ~= nil then
|
|
414
|
+
local t = typeof(curVal)
|
|
415
|
+
if t == "Rect" then
|
|
416
|
+
pcall(function()
|
|
417
|
+
instance[key] = resolveRect(value)
|
|
418
|
+
end)
|
|
419
|
+
return
|
|
420
|
+
elseif t == "UDim2" then
|
|
421
|
+
pcall(function()
|
|
422
|
+
instance[key] = resolveUDim2(value)
|
|
423
|
+
end)
|
|
424
|
+
return
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
-- Default 4-element to UDim2
|
|
428
|
+
pcall(function()
|
|
429
|
+
instance[key] = resolveUDim2(value)
|
|
430
|
+
end)
|
|
431
|
+
return
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
-- Direct assignment for numbers, booleans, etc.
|
|
435
|
+
pcall(function()
|
|
436
|
+
instance[key] = value
|
|
437
|
+
end)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
local RESERVED_KEYS = {
|
|
441
|
+
ClassName = true,
|
|
442
|
+
className = true,
|
|
443
|
+
Name = true,
|
|
444
|
+
name = true,
|
|
445
|
+
Children = true,
|
|
446
|
+
children = true,
|
|
447
|
+
properties = true,
|
|
448
|
+
Properties = true,
|
|
449
|
+
props = true,
|
|
450
|
+
Props = true,
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
local function buildNode(spec, parent, animate)
|
|
454
|
+
local className = spec.ClassName or spec.className or "Frame"
|
|
455
|
+
local name = spec.Name or spec.name or className
|
|
456
|
+
|
|
457
|
+
local instance = Instance.new(className)
|
|
458
|
+
instance.Name = name
|
|
459
|
+
|
|
460
|
+
-- Apply flattened top-level properties
|
|
461
|
+
for key, value in spec do
|
|
462
|
+
if not RESERVED_KEYS[key] then
|
|
463
|
+
applyProperty(instance, key, value)
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
-- Also accept a nested "properties" / "Properties" / "props" bag for convenience.
|
|
468
|
+
-- This lets callers group properties explicitly without the key being misread
|
|
469
|
+
-- as an instance property name.
|
|
470
|
+
local propBag = spec.properties or spec.Properties or spec.props or spec.Props
|
|
471
|
+
if type(propBag) == "table" then
|
|
472
|
+
for key, value in propBag do
|
|
473
|
+
if not RESERVED_KEYS[key] then
|
|
474
|
+
applyProperty(instance, key, value)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
-- Set parent after properties to avoid unnecessary layout computations
|
|
480
|
+
instance.Parent = parent
|
|
481
|
+
|
|
482
|
+
-- Build children recursively
|
|
483
|
+
local children = spec.Children or spec.children
|
|
484
|
+
if children then
|
|
485
|
+
for _, childSpec in children do
|
|
486
|
+
if animate then
|
|
487
|
+
task.wait(0.05)
|
|
488
|
+
end
|
|
489
|
+
buildNode(childSpec, instance, animate)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
return instance
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
function UIBuilder.build(spec)
|
|
497
|
+
local parentPath = spec.parent or "StarterGui"
|
|
498
|
+
local parent = Explorer.resolveInstance(parentPath)
|
|
499
|
+
if not parent then
|
|
500
|
+
-- Try as a service
|
|
501
|
+
local ok
|
|
502
|
+
ok, parent = pcall(function()
|
|
503
|
+
return game:GetService(parentPath)
|
|
504
|
+
end)
|
|
505
|
+
if not ok then
|
|
506
|
+
return { success = false, error = "Parent not found: " .. parentPath }
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
-- Clean up existing instance with same name
|
|
511
|
+
local existingName = spec.tree and (spec.tree.Name or spec.tree.name) or nil
|
|
512
|
+
if existingName then
|
|
513
|
+
local existing = parent:FindFirstChild(existingName)
|
|
514
|
+
if existing then
|
|
515
|
+
existing:Destroy()
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
local tree = spec.tree
|
|
520
|
+
if not tree then
|
|
521
|
+
return { success = false, error = "No 'tree' provided in spec" }
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
local animate = false
|
|
525
|
+
if spec.animate ~= nil then
|
|
526
|
+
animate = spec.animate
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
local ok, result = pcall(function()
|
|
530
|
+
return buildNode(tree, parent, animate)
|
|
531
|
+
end)
|
|
532
|
+
|
|
533
|
+
if not ok then
|
|
534
|
+
return { success = false, error = "Build failed: " .. tostring(result) }
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
local path = Explorer.getPath(result)
|
|
538
|
+
local count = 0
|
|
539
|
+
local function countDescendants(inst)
|
|
540
|
+
count = count + 1
|
|
541
|
+
for _, child in inst:GetChildren() do
|
|
542
|
+
countDescendants(child)
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
countDescendants(result)
|
|
546
|
+
|
|
547
|
+
return { success = true, path = path, instanceCount = count }
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
--[[
|
|
551
|
+
Serialize: convert an existing instance tree into create_ui-compatible JSON.
|
|
552
|
+
Reads properties via ReflectionService, converts Roblox types to JSON-friendly values,
|
|
553
|
+
and skips default/unchanged values to keep output minimal.
|
|
554
|
+
]]
|
|
555
|
+
|
|
556
|
+
local Reflection = require(script.Parent.Reflection)
|
|
557
|
+
local ReflectionService = game:GetService("ReflectionService")
|
|
558
|
+
|
|
559
|
+
-- Convert a Roblox value to a JSON-friendly representation
|
|
560
|
+
local function valueToJson(value, propType)
|
|
561
|
+
local t = typeof(value)
|
|
562
|
+
|
|
563
|
+
if t == "UDim2" then
|
|
564
|
+
return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
|
|
565
|
+
end
|
|
566
|
+
if t == "UDim" then
|
|
567
|
+
return { value.Scale, value.Offset }
|
|
568
|
+
end
|
|
569
|
+
if t == "Vector2" then
|
|
570
|
+
return { value.X, value.Y }
|
|
571
|
+
end
|
|
572
|
+
if t == "Vector3" then
|
|
573
|
+
return { value.X, value.Y, value.Z }
|
|
574
|
+
end
|
|
575
|
+
if t == "Color3" then
|
|
576
|
+
return "#" .. value:ToHex()
|
|
577
|
+
end
|
|
578
|
+
if t == "BrickColor" then
|
|
579
|
+
return value.Name
|
|
580
|
+
end
|
|
581
|
+
if t == "ColorSequence" then
|
|
582
|
+
local kps = {}
|
|
583
|
+
for _, kp in value.Keypoints do
|
|
584
|
+
table.insert(kps, { kp.Time, "#" .. kp.Value:ToHex() })
|
|
585
|
+
end
|
|
586
|
+
return kps
|
|
587
|
+
end
|
|
588
|
+
if t == "NumberSequence" then
|
|
589
|
+
local kps = {}
|
|
590
|
+
for _, kp in value.Keypoints do
|
|
591
|
+
table.insert(kps, { kp.Time, kp.Value, kp.Envelope })
|
|
592
|
+
end
|
|
593
|
+
return kps
|
|
594
|
+
end
|
|
595
|
+
if t == "NumberRange" then
|
|
596
|
+
return { value.Min, value.Max }
|
|
597
|
+
end
|
|
598
|
+
if t == "Rect" then
|
|
599
|
+
return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y }
|
|
600
|
+
end
|
|
601
|
+
if t == "CFrame" then
|
|
602
|
+
return { value:GetComponents() }
|
|
603
|
+
end
|
|
604
|
+
if t == "EnumItem" then
|
|
605
|
+
return value.Name
|
|
606
|
+
end
|
|
607
|
+
if t == "Font" then
|
|
608
|
+
return {
|
|
609
|
+
Family = value.Family,
|
|
610
|
+
Weight = value.Weight.Name,
|
|
611
|
+
Style = value.Style.Name,
|
|
612
|
+
}
|
|
613
|
+
end
|
|
614
|
+
if t == "boolean" or t == "number" or t == "string" then
|
|
615
|
+
return value
|
|
616
|
+
end
|
|
617
|
+
-- Fallback: tostring
|
|
618
|
+
return tostring(value)
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
local SERIALIZE_SKIP = {
|
|
622
|
+
Parent = true,
|
|
623
|
+
ClassName = true,
|
|
624
|
+
Name = true,
|
|
625
|
+
Archivable = true,
|
|
626
|
+
AbsolutePosition = true,
|
|
627
|
+
AbsoluteSize = true,
|
|
628
|
+
AbsoluteRotation = true,
|
|
629
|
+
IsLoaded = true,
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
local function serializeNode(instance, maxDepth, depth)
|
|
633
|
+
if depth > maxDepth then
|
|
634
|
+
return nil
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
local node = {
|
|
638
|
+
ClassName = instance.ClassName,
|
|
639
|
+
Name = instance.Name,
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
-- Get non-default properties via reflection
|
|
643
|
+
local ok, propData = pcall(function()
|
|
644
|
+
return ReflectionService:GetPropertiesOfClass(instance.ClassName)
|
|
645
|
+
end)
|
|
646
|
+
|
|
647
|
+
if ok and propData then
|
|
648
|
+
-- Create a default instance to compare against
|
|
649
|
+
local defaultInstance = nil
|
|
650
|
+
pcall(function()
|
|
651
|
+
defaultInstance = Instance.new(instance.ClassName)
|
|
652
|
+
end)
|
|
653
|
+
|
|
654
|
+
for _, prop in propData do
|
|
655
|
+
if not SERIALIZE_SKIP[prop.Name] and not prop.Name:match("^Absolute") then
|
|
656
|
+
local valOk, value = pcall(function()
|
|
657
|
+
return instance[prop.Name]
|
|
658
|
+
end)
|
|
659
|
+
if valOk and value ~= nil then
|
|
660
|
+
-- Skip default values
|
|
661
|
+
local isDefault = false
|
|
662
|
+
if defaultInstance then
|
|
663
|
+
local defOk, defVal = pcall(function()
|
|
664
|
+
return defaultInstance[prop.Name]
|
|
665
|
+
end)
|
|
666
|
+
if defOk and defVal == value then
|
|
667
|
+
isDefault = true
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
if not isDefault then
|
|
672
|
+
local jsonVal = valueToJson(value, prop.Type and tostring(prop.Type))
|
|
673
|
+
if jsonVal ~= nil then
|
|
674
|
+
node[prop.Name] = jsonVal
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
if defaultInstance then
|
|
682
|
+
pcall(function()
|
|
683
|
+
defaultInstance:Destroy()
|
|
684
|
+
end)
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
-- Serialize children
|
|
689
|
+
local children = instance:GetChildren()
|
|
690
|
+
if #children > 0 then
|
|
691
|
+
node.Children = {}
|
|
692
|
+
for _, child in children do
|
|
693
|
+
local childNode = serializeNode(child, maxDepth, depth + 1)
|
|
694
|
+
if childNode then
|
|
695
|
+
table.insert(node.Children, childNode)
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
if #node.Children == 0 then
|
|
699
|
+
node.Children = nil
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
return node
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
function UIBuilder.serialize(spec)
|
|
707
|
+
local path = spec.path
|
|
708
|
+
if not path then
|
|
709
|
+
return { success = false, error = "Missing 'path' parameter" }
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
local instance = Explorer.resolveInstance(path)
|
|
713
|
+
if not instance then
|
|
714
|
+
return { success = false, error = "Instance not found: " .. path }
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
local maxDepth = spec.maxDepth or 50
|
|
718
|
+
|
|
719
|
+
local ok, tree = pcall(function()
|
|
720
|
+
return serializeNode(instance, maxDepth, 0)
|
|
721
|
+
end)
|
|
722
|
+
|
|
723
|
+
if not ok then
|
|
724
|
+
return { success = false, error = "Serialization failed: " .. tostring(tree) }
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
return { success = true, tree = tree }
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
local function buildStrictNode(spec, state, depth)
|
|
731
|
+
assert(type(spec) == "table", "Every UI node must be an object")
|
|
732
|
+
if depth > state.maxDepth then
|
|
733
|
+
error("UI tree exceeds maximum depth of " .. state.maxDepth)
|
|
734
|
+
end
|
|
735
|
+
state.count += 1
|
|
736
|
+
if state.count > state.maxNodes then
|
|
737
|
+
error("UI tree exceeds maximum node count of " .. state.maxNodes)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
local className = spec.ClassName or spec.className
|
|
741
|
+
assert(type(className) == "string", "Every UI node requires ClassName")
|
|
742
|
+
local instance = Instance.new(className)
|
|
743
|
+
instance.Name = spec.Name or spec.name or className
|
|
744
|
+
|
|
745
|
+
for key, value in spec do
|
|
746
|
+
if not RESERVED_KEYS[key] then
|
|
747
|
+
ValueCodec.setProperty(instance, key, value)
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
local properties = spec.properties or spec.Properties or spec.props or spec.Props
|
|
751
|
+
if properties ~= nil then
|
|
752
|
+
assert(type(properties) == "table", "UI node properties must be an object")
|
|
753
|
+
for key, value in properties do
|
|
754
|
+
ValueCodec.setProperty(instance, key, value)
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
local children = spec.Children or spec.children
|
|
759
|
+
if children ~= nil then
|
|
760
|
+
assert(type(children) == "table", "UI node Children must be an array")
|
|
761
|
+
for _, childSpec in children do
|
|
762
|
+
local child = buildStrictNode(childSpec, state, depth + 1)
|
|
763
|
+
child.Parent = instance
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
return instance
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
function UIBuilder.buildV2(spec)
|
|
770
|
+
if type(spec.tree) ~= "table" then
|
|
771
|
+
return { success = false, error = "tree is required" }
|
|
772
|
+
end
|
|
773
|
+
local parentRef = spec.parent or { pathSegments = { "StarterGui" } }
|
|
774
|
+
local parent, parentErr = InstanceRegistry.resolve(parentRef)
|
|
775
|
+
if not parent then
|
|
776
|
+
return { success = false, error = "Parent: " .. tostring(parentErr) }
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
local state = {
|
|
780
|
+
count = 0,
|
|
781
|
+
maxDepth = math.clamp(spec.maxDepth or 30, 1, 50),
|
|
782
|
+
maxNodes = math.clamp(spec.maxNodes or 1000, 1, 3000),
|
|
783
|
+
}
|
|
784
|
+
local builtOk, rootOrError = pcall(function()
|
|
785
|
+
return buildStrictNode(spec.tree, state, 0)
|
|
786
|
+
end)
|
|
787
|
+
if not builtOk then
|
|
788
|
+
return { success = false, error = "UI validation failed: " .. tostring(rootOrError) }
|
|
789
|
+
end
|
|
790
|
+
local root = rootOrError
|
|
791
|
+
|
|
792
|
+
local existing = nil
|
|
793
|
+
local duplicateCount = 0
|
|
794
|
+
for _, child in parent:GetChildren() do
|
|
795
|
+
if child.Name == root.Name then
|
|
796
|
+
existing = child
|
|
797
|
+
duplicateCount += 1
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
if duplicateCount > 1 then
|
|
801
|
+
root:Destroy()
|
|
802
|
+
return {
|
|
803
|
+
success = false,
|
|
804
|
+
error = "Replacement target is ambiguous because multiple children share the name " .. root.Name,
|
|
805
|
+
}
|
|
806
|
+
end
|
|
807
|
+
if existing and spec.replaceExisting ~= true then
|
|
808
|
+
root:Destroy()
|
|
809
|
+
return {
|
|
810
|
+
success = false,
|
|
811
|
+
error = "A child named " .. root.Name .. " already exists; set replaceExisting=true to replace it",
|
|
812
|
+
}
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
local recording = ChangeHistoryService:TryBeginRecording("Dominus 2: Build UI " .. root.Name)
|
|
816
|
+
if not recording then
|
|
817
|
+
root:Destroy()
|
|
818
|
+
return { success = false, error = "Another plugin recording is already active" }
|
|
819
|
+
end
|
|
820
|
+
local commitOk, commitErr = pcall(function()
|
|
821
|
+
if existing then
|
|
822
|
+
existing:Destroy()
|
|
823
|
+
end
|
|
824
|
+
root.Parent = parent
|
|
825
|
+
end)
|
|
826
|
+
if not commitOk then
|
|
827
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
828
|
+
if root.Parent == nil then
|
|
829
|
+
root:Destroy()
|
|
830
|
+
end
|
|
831
|
+
return { success = false, error = "UI commit failed: " .. tostring(commitErr), rolledBack = true }
|
|
832
|
+
end
|
|
833
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
834
|
+
return {
|
|
835
|
+
success = true,
|
|
836
|
+
root = InstanceRegistry.toRef(root),
|
|
837
|
+
instanceCount = state.count,
|
|
838
|
+
replaced = existing ~= nil,
|
|
839
|
+
}
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
function UIBuilder.snapshotV2(spec)
|
|
843
|
+
local instance, err = InstanceRegistry.resolve(spec.target)
|
|
844
|
+
if not instance then
|
|
845
|
+
return { success = false, error = err }
|
|
846
|
+
end
|
|
847
|
+
local maxDepth = math.clamp(spec.maxDepth or 30, 0, 50)
|
|
848
|
+
local ok, tree = pcall(function()
|
|
849
|
+
return serializeNode(instance, maxDepth, 0)
|
|
850
|
+
end)
|
|
851
|
+
if not ok then
|
|
852
|
+
return { success = false, error = "UI serialization failed: " .. tostring(tree) }
|
|
853
|
+
end
|
|
854
|
+
return { success = true, target = InstanceRegistry.toRef(instance), tree = tree }
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
return UIBuilder
|