dominus-cli 0.2.1 → 0.5.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/dist/index.js +423 -70
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +18 -3
- package/dist/mcp.js.map +1 -1
- package/package.json +17 -16
- package/plugin/Dominus.rbxm +0 -0
- package/plugin/src/Executor.lua +1 -0
- package/plugin/src/Explorer.lua +123 -1
- package/plugin/src/Properties.lua +213 -28
- package/plugin/src/UIBuilder.lua +494 -32
- package/plugin/src/init.server.lua +111 -22
package/plugin/src/UIBuilder.lua
CHANGED
|
@@ -4,18 +4,32 @@
|
|
|
4
4
|
]]
|
|
5
5
|
|
|
6
6
|
local Explorer = require(script.Parent.Explorer)
|
|
7
|
+
local Reflection = require(script.Parent.Reflection)
|
|
7
8
|
|
|
8
9
|
local UIBuilder = {}
|
|
9
10
|
|
|
11
|
+
-- Cache property type maps per ClassName to avoid repeated reflection calls
|
|
12
|
+
local propertyTypeCache = {}
|
|
13
|
+
local function getPropertyType(className, propName)
|
|
14
|
+
if not propertyTypeCache[className] then
|
|
15
|
+
propertyTypeCache[className] = Reflection.getPropertyTypes(className)
|
|
16
|
+
end
|
|
17
|
+
return propertyTypeCache[className][propName]
|
|
18
|
+
end
|
|
19
|
+
|
|
10
20
|
local function resolveColor(value)
|
|
11
21
|
if type(value) == "string" then
|
|
12
22
|
if value:sub(1, 1) == "#" then
|
|
13
23
|
return Color3.fromHex(value)
|
|
14
24
|
end
|
|
15
25
|
local r, g, b = value:match("rgb%s*%((%d+)%D+(%d+)%D+(%d+)%)")
|
|
16
|
-
if r then
|
|
26
|
+
if r then
|
|
27
|
+
return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))
|
|
28
|
+
end
|
|
17
29
|
local ok, bc = pcall(BrickColor.new, value)
|
|
18
|
-
if ok then
|
|
30
|
+
if ok then
|
|
31
|
+
return bc.Color
|
|
32
|
+
end
|
|
19
33
|
end
|
|
20
34
|
if type(value) == "table" then
|
|
21
35
|
if value[1] then
|
|
@@ -35,14 +49,18 @@ local function resolveUDim2(value)
|
|
|
35
49
|
if type(value) == "table" then
|
|
36
50
|
if value.XScale or value.xScale then
|
|
37
51
|
return UDim2.new(
|
|
38
|
-
value.XScale or value.xScale or 0,
|
|
39
|
-
value.
|
|
52
|
+
value.XScale or value.xScale or 0,
|
|
53
|
+
value.XOffset or value.xOffset or 0,
|
|
54
|
+
value.YScale or value.yScale or 0,
|
|
55
|
+
value.YOffset or value.yOffset or 0
|
|
40
56
|
)
|
|
41
57
|
end
|
|
42
58
|
if value.X and type(value.X) == "table" then
|
|
43
59
|
return UDim2.new(
|
|
44
|
-
value.X.Scale or value.X[1] or 0,
|
|
45
|
-
value.
|
|
60
|
+
value.X.Scale or value.X[1] or 0,
|
|
61
|
+
value.X.Offset or value.X[2] or 0,
|
|
62
|
+
value.Y and (value.Y.Scale or value.Y[1]) or 0,
|
|
63
|
+
value.Y and (value.Y.Offset or value.Y[2]) or 0
|
|
46
64
|
)
|
|
47
65
|
end
|
|
48
66
|
if value[1] ~= nil then
|
|
@@ -51,7 +69,9 @@ local function resolveUDim2(value)
|
|
|
51
69
|
end
|
|
52
70
|
if type(value) == "string" then
|
|
53
71
|
local a, b, c, d = value:match("([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)")
|
|
54
|
-
if a then
|
|
72
|
+
if a then
|
|
73
|
+
return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d))
|
|
74
|
+
end
|
|
55
75
|
end
|
|
56
76
|
return UDim2.new(0, 0, 0, 0)
|
|
57
77
|
end
|
|
@@ -74,42 +94,187 @@ local function resolveVector2(value)
|
|
|
74
94
|
end
|
|
75
95
|
|
|
76
96
|
local function resolveEnum(instance, key, value)
|
|
77
|
-
if type(value) ~= "string" then
|
|
78
|
-
|
|
97
|
+
if type(value) ~= "string" then
|
|
98
|
+
return value
|
|
99
|
+
end
|
|
100
|
+
local curOk, curVal = pcall(function()
|
|
101
|
+
return instance[key]
|
|
102
|
+
end)
|
|
79
103
|
if curOk and typeof(curVal) == "EnumItem" then
|
|
80
104
|
local enumType = tostring(curVal.EnumType)
|
|
81
|
-
local ok, enumVal = pcall(function()
|
|
82
|
-
|
|
105
|
+
local ok, enumVal = pcall(function()
|
|
106
|
+
return Enum[enumType][value]
|
|
107
|
+
end)
|
|
108
|
+
if ok then
|
|
109
|
+
return enumVal
|
|
110
|
+
end
|
|
83
111
|
end
|
|
84
112
|
return value
|
|
85
113
|
end
|
|
86
114
|
|
|
87
115
|
local function resolveFont(value)
|
|
88
116
|
if type(value) == "string" then
|
|
89
|
-
local ok, font = pcall(function()
|
|
90
|
-
|
|
117
|
+
local ok, font = pcall(function()
|
|
118
|
+
return Enum.Font[value]
|
|
119
|
+
end)
|
|
120
|
+
if ok then
|
|
121
|
+
return font
|
|
122
|
+
end
|
|
91
123
|
end
|
|
92
124
|
return value
|
|
93
125
|
end
|
|
94
126
|
|
|
95
127
|
local UDIM2_PROPS = {
|
|
96
|
-
Size = true,
|
|
128
|
+
Size = true,
|
|
129
|
+
Position = true,
|
|
130
|
+
CellSize = true,
|
|
131
|
+
CellPadding = true,
|
|
97
132
|
}
|
|
98
133
|
|
|
99
|
-
local
|
|
100
|
-
BackgroundColor3 = true,
|
|
101
|
-
|
|
102
|
-
|
|
134
|
+
local COLOR3_PROPS = {
|
|
135
|
+
BackgroundColor3 = true,
|
|
136
|
+
BorderColor3 = true,
|
|
137
|
+
TextColor3 = true,
|
|
138
|
+
ImageColor3 = true,
|
|
139
|
+
PlaceholderColor3 = true,
|
|
140
|
+
TextStrokeColor3 = true,
|
|
141
|
+
ScrollBarImageColor3 = true,
|
|
103
142
|
}
|
|
104
143
|
|
|
144
|
+
local function resolveColorSequence(value)
|
|
145
|
+
if type(value) == "table" then
|
|
146
|
+
-- Array of keypoints: {{time, color}, ...}
|
|
147
|
+
if type(value[1]) == "table" then
|
|
148
|
+
local keypoints = {}
|
|
149
|
+
for _, kp in ipairs(value) do
|
|
150
|
+
local t = kp[1] or kp.Time or 0
|
|
151
|
+
local c = resolveColor(kp[2] or kp.Color or kp)
|
|
152
|
+
table.insert(keypoints, ColorSequenceKeypoint.new(t, c))
|
|
153
|
+
end
|
|
154
|
+
return ColorSequence.new(keypoints)
|
|
155
|
+
end
|
|
156
|
+
-- Single color as array [r,g,b] → uniform ColorSequence
|
|
157
|
+
if type(value[1]) == "number" and #value == 3 then
|
|
158
|
+
local c = resolveColor(value)
|
|
159
|
+
return ColorSequence.new(c)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
-- String hex/rgb → uniform ColorSequence
|
|
163
|
+
if type(value) == "string" then
|
|
164
|
+
return ColorSequence.new(resolveColor(value))
|
|
165
|
+
end
|
|
166
|
+
return ColorSequence.new(Color3.new(1, 1, 1))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
local function resolveNumberSequence(value)
|
|
170
|
+
if type(value) == "table" then
|
|
171
|
+
-- Array of keypoints: {{time, value, envelope?}, ...}
|
|
172
|
+
if type(value[1]) == "table" then
|
|
173
|
+
local keypoints = {}
|
|
174
|
+
for _, kp in ipairs(value) do
|
|
175
|
+
local t = kp[1] or kp.Time or 0
|
|
176
|
+
local v = kp[2] or kp.Value or 0
|
|
177
|
+
local e = kp[3] or kp.Envelope or 0
|
|
178
|
+
table.insert(keypoints, NumberSequenceKeypoint.new(t, v, e))
|
|
179
|
+
end
|
|
180
|
+
return NumberSequence.new(keypoints)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
if type(value) == "number" then
|
|
184
|
+
return NumberSequence.new(value)
|
|
185
|
+
end
|
|
186
|
+
return NumberSequence.new(0)
|
|
187
|
+
end
|
|
188
|
+
|
|
105
189
|
local VECTOR2_PROPS = {
|
|
106
|
-
AnchorPoint = true,
|
|
190
|
+
AnchorPoint = true,
|
|
191
|
+
CanvasSize_v2 = true,
|
|
192
|
+
AbsoluteSize = true,
|
|
193
|
+
AbsolutePosition = true,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
local UDIM_PROPS = {
|
|
197
|
+
CornerRadius = true,
|
|
198
|
+
Padding = true,
|
|
199
|
+
PaddingTop = true,
|
|
200
|
+
PaddingBottom = true,
|
|
201
|
+
PaddingLeft = true,
|
|
202
|
+
PaddingRight = true,
|
|
203
|
+
FillDirection = false, -- not UDim
|
|
107
204
|
}
|
|
108
205
|
|
|
206
|
+
local NUMBER_RANGE_PROPS = {
|
|
207
|
+
Range = true,
|
|
208
|
+
RotationRange = true,
|
|
209
|
+
Size_NR = true, -- particle size range alias
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
local RECT_PROPS = {
|
|
213
|
+
SliceCenter = true,
|
|
214
|
+
ImageRectOffset_Rect = true,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
local function resolveNumberRange(value)
|
|
218
|
+
if type(value) == "table" then
|
|
219
|
+
return NumberRange.new(value[1] or 0, value[2] or value[1] or 0)
|
|
220
|
+
end
|
|
221
|
+
if type(value) == "number" then
|
|
222
|
+
return NumberRange.new(value)
|
|
223
|
+
end
|
|
224
|
+
return NumberRange.new(0)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
local function resolveRect(value)
|
|
228
|
+
if type(value) == "table" then
|
|
229
|
+
if #value == 4 then
|
|
230
|
+
return Rect.new(value[1], value[2], value[3], value[4])
|
|
231
|
+
end
|
|
232
|
+
return Rect.new(
|
|
233
|
+
value.Min and value.Min[1] or value.MinX or 0,
|
|
234
|
+
value.Min and value.Min[2] or value.MinY or 0,
|
|
235
|
+
value.Max and value.Max[1] or value.MaxX or 0,
|
|
236
|
+
value.Max and value.Max[2] or value.MaxY or 0
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
return Rect.new(0, 0, 0, 0)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
-- Disambiguate 2-element arrays: check actual property type on the instance
|
|
243
|
+
local function resolve2ElementArray(instance, key, value)
|
|
244
|
+
-- First, check our known lookup tables
|
|
245
|
+
if VECTOR2_PROPS[key] then
|
|
246
|
+
return resolveVector2(value)
|
|
247
|
+
end
|
|
248
|
+
if UDIM_PROPS[key] then
|
|
249
|
+
return resolveUDim(value)
|
|
250
|
+
end
|
|
251
|
+
if NUMBER_RANGE_PROPS[key] then
|
|
252
|
+
return resolveNumberRange(value)
|
|
253
|
+
end
|
|
254
|
+
-- Fall back to runtime type introspection
|
|
255
|
+
local ok, curVal = pcall(function()
|
|
256
|
+
return instance[key]
|
|
257
|
+
end)
|
|
258
|
+
if ok and curVal ~= nil then
|
|
259
|
+
local t = typeof(curVal)
|
|
260
|
+
if t == "Vector2" then
|
|
261
|
+
return resolveVector2(value)
|
|
262
|
+
end
|
|
263
|
+
if t == "UDim" then
|
|
264
|
+
return resolveUDim(value)
|
|
265
|
+
end
|
|
266
|
+
if t == "NumberRange" then
|
|
267
|
+
return resolveNumberRange(value)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
-- Default: assume UDim for 2-element (more common in UI)
|
|
271
|
+
return resolveUDim(value)
|
|
272
|
+
end
|
|
273
|
+
|
|
109
274
|
local function applyProperty(instance, key, value)
|
|
110
275
|
-- Special handling by property name
|
|
111
276
|
if key == "Size" or key == "Position" then
|
|
112
|
-
if instance:IsA("GuiObject") or instance:IsA("
|
|
277
|
+
if instance:IsA("GuiObject") or instance:IsA("UIBase") then
|
|
113
278
|
instance[key] = resolveUDim2(value)
|
|
114
279
|
return
|
|
115
280
|
end
|
|
@@ -127,16 +292,63 @@ local function applyProperty(instance, key, value)
|
|
|
127
292
|
return
|
|
128
293
|
end
|
|
129
294
|
|
|
130
|
-
if key
|
|
295
|
+
if UDIM_PROPS[key] then
|
|
131
296
|
instance[key] = resolveUDim(value)
|
|
132
297
|
return
|
|
133
298
|
end
|
|
134
299
|
|
|
135
|
-
|
|
300
|
+
-- Use reflection to distinguish Color3 vs ColorSequence, NumberSequence, etc.
|
|
301
|
+
local propType = getPropertyType(instance.ClassName, key)
|
|
302
|
+
|
|
303
|
+
if propType == "ColorSequence" then
|
|
304
|
+
instance[key] = resolveColorSequence(value)
|
|
305
|
+
return
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
if propType == "NumberSequence" then
|
|
309
|
+
instance[key] = resolveNumberSequence(value)
|
|
310
|
+
return
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
if COLOR3_PROPS[key] or propType == "Color3" then
|
|
136
314
|
instance[key] = resolveColor(value)
|
|
137
315
|
return
|
|
138
316
|
end
|
|
139
317
|
|
|
318
|
+
-- Reflection-based coercion for types not in hardcoded tables
|
|
319
|
+
if propType == "UDim2" and type(value) == "table" then
|
|
320
|
+
instance[key] = resolveUDim2(value)
|
|
321
|
+
return
|
|
322
|
+
end
|
|
323
|
+
if propType == "UDim" then
|
|
324
|
+
instance[key] = resolveUDim(value)
|
|
325
|
+
return
|
|
326
|
+
end
|
|
327
|
+
if propType == "Vector2" then
|
|
328
|
+
instance[key] = resolveVector2(value)
|
|
329
|
+
return
|
|
330
|
+
end
|
|
331
|
+
if propType == "NumberRange" then
|
|
332
|
+
instance[key] = resolveNumberRange(value)
|
|
333
|
+
return
|
|
334
|
+
end
|
|
335
|
+
if propType == "Rect" then
|
|
336
|
+
instance[key] = resolveRect(value)
|
|
337
|
+
return
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
-- Rect properties (4-element arrays like SliceCenter)
|
|
341
|
+
if RECT_PROPS[key] then
|
|
342
|
+
instance[key] = resolveRect(value)
|
|
343
|
+
return
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
-- NumberRange properties
|
|
347
|
+
if NUMBER_RANGE_PROPS[key] then
|
|
348
|
+
instance[key] = resolveNumberRange(value)
|
|
349
|
+
return
|
|
350
|
+
end
|
|
351
|
+
|
|
140
352
|
if key == "Font" and instance:IsA("TextLabel") or instance:IsA("TextButton") or instance:IsA("TextBox") then
|
|
141
353
|
instance[key] = resolveFont(value)
|
|
142
354
|
return
|
|
@@ -147,8 +359,12 @@ local function applyProperty(instance, key, value)
|
|
|
147
359
|
local family = value.Family or "rbxasset://fonts/families/SourceSansPro.json"
|
|
148
360
|
local weight = Enum.FontWeight.Regular
|
|
149
361
|
local style = Enum.FontStyle.Normal
|
|
150
|
-
pcall(function()
|
|
151
|
-
|
|
362
|
+
pcall(function()
|
|
363
|
+
weight = Enum.FontWeight[value.Weight or "Regular"]
|
|
364
|
+
end)
|
|
365
|
+
pcall(function()
|
|
366
|
+
style = Enum.FontStyle[value.Style or "Normal"]
|
|
367
|
+
end)
|
|
152
368
|
instance.FontFace = Font.new(family, weight, style)
|
|
153
369
|
end
|
|
154
370
|
return
|
|
@@ -157,19 +373,68 @@ local function applyProperty(instance, key, value)
|
|
|
157
373
|
-- Enum properties (string values)
|
|
158
374
|
if type(value) == "string" then
|
|
159
375
|
local resolved = resolveEnum(instance, key, value)
|
|
160
|
-
local ok, err = pcall(function()
|
|
376
|
+
local ok, err = pcall(function()
|
|
377
|
+
instance[key] = resolved
|
|
378
|
+
end)
|
|
161
379
|
if not ok then
|
|
162
380
|
-- Maybe it's a direct value
|
|
163
|
-
pcall(function()
|
|
381
|
+
pcall(function()
|
|
382
|
+
instance[key] = value
|
|
383
|
+
end)
|
|
164
384
|
end
|
|
165
385
|
return
|
|
166
386
|
end
|
|
167
387
|
|
|
388
|
+
if UDIM2_PROPS[key] then
|
|
389
|
+
pcall(function()
|
|
390
|
+
instance[key] = resolveUDim2(value)
|
|
391
|
+
end)
|
|
392
|
+
return
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
-- Smart disambiguation for 2-element table arrays
|
|
396
|
+
if type(value) == "table" and #value == 2 then
|
|
397
|
+
local resolved = resolve2ElementArray(instance, key, value)
|
|
398
|
+
pcall(function()
|
|
399
|
+
instance[key] = resolved
|
|
400
|
+
end)
|
|
401
|
+
return
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
-- 4-element table arrays: try Rect if not already handled as UDim2
|
|
405
|
+
if type(value) == "table" and #value == 4 then
|
|
406
|
+
-- Check actual property type
|
|
407
|
+
local ok, curVal = pcall(function()
|
|
408
|
+
return instance[key]
|
|
409
|
+
end)
|
|
410
|
+
if ok and curVal ~= nil then
|
|
411
|
+
local t = typeof(curVal)
|
|
412
|
+
if t == "Rect" then
|
|
413
|
+
pcall(function()
|
|
414
|
+
instance[key] = resolveRect(value)
|
|
415
|
+
end)
|
|
416
|
+
return
|
|
417
|
+
elseif t == "UDim2" then
|
|
418
|
+
pcall(function()
|
|
419
|
+
instance[key] = resolveUDim2(value)
|
|
420
|
+
end)
|
|
421
|
+
return
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
-- Default 4-element to UDim2
|
|
425
|
+
pcall(function()
|
|
426
|
+
instance[key] = resolveUDim2(value)
|
|
427
|
+
end)
|
|
428
|
+
return
|
|
429
|
+
end
|
|
430
|
+
|
|
168
431
|
-- Direct assignment for numbers, booleans, etc.
|
|
169
|
-
pcall(function()
|
|
432
|
+
pcall(function()
|
|
433
|
+
instance[key] = value
|
|
434
|
+
end)
|
|
170
435
|
end
|
|
171
436
|
|
|
172
|
-
local function buildNode(spec, parent)
|
|
437
|
+
local function buildNode(spec, parent, animate)
|
|
173
438
|
local className = spec.ClassName or spec.className or "Frame"
|
|
174
439
|
local name = spec.Name or spec.name or className
|
|
175
440
|
|
|
@@ -178,7 +443,14 @@ local function buildNode(spec, parent)
|
|
|
178
443
|
|
|
179
444
|
-- Apply all properties
|
|
180
445
|
for key, value in spec do
|
|
181
|
-
if
|
|
446
|
+
if
|
|
447
|
+
key ~= "ClassName"
|
|
448
|
+
and key ~= "className"
|
|
449
|
+
and key ~= "Name"
|
|
450
|
+
and key ~= "name"
|
|
451
|
+
and key ~= "Children"
|
|
452
|
+
and key ~= "children"
|
|
453
|
+
then
|
|
182
454
|
applyProperty(instance, key, value)
|
|
183
455
|
end
|
|
184
456
|
end
|
|
@@ -190,7 +462,10 @@ local function buildNode(spec, parent)
|
|
|
190
462
|
local children = spec.Children or spec.children
|
|
191
463
|
if children then
|
|
192
464
|
for _, childSpec in children do
|
|
193
|
-
|
|
465
|
+
if animate then
|
|
466
|
+
task.wait(0.05)
|
|
467
|
+
end
|
|
468
|
+
buildNode(childSpec, instance, animate)
|
|
194
469
|
end
|
|
195
470
|
end
|
|
196
471
|
|
|
@@ -203,7 +478,9 @@ function UIBuilder.build(spec)
|
|
|
203
478
|
if not parent then
|
|
204
479
|
-- Try as a service
|
|
205
480
|
local ok
|
|
206
|
-
ok, parent = pcall(function()
|
|
481
|
+
ok, parent = pcall(function()
|
|
482
|
+
return game:GetService(parentPath)
|
|
483
|
+
end)
|
|
207
484
|
if not ok then
|
|
208
485
|
return { success = false, error = "Parent not found: " .. parentPath }
|
|
209
486
|
end
|
|
@@ -223,8 +500,13 @@ function UIBuilder.build(spec)
|
|
|
223
500
|
return { success = false, error = "No 'tree' provided in spec" }
|
|
224
501
|
end
|
|
225
502
|
|
|
503
|
+
local animate = false
|
|
504
|
+
if spec.animate ~= nil then
|
|
505
|
+
animate = spec.animate
|
|
506
|
+
end
|
|
507
|
+
|
|
226
508
|
local ok, result = pcall(function()
|
|
227
|
-
return buildNode(tree, parent)
|
|
509
|
+
return buildNode(tree, parent, animate)
|
|
228
510
|
end)
|
|
229
511
|
|
|
230
512
|
if not ok then
|
|
@@ -244,4 +526,184 @@ function UIBuilder.build(spec)
|
|
|
244
526
|
return { success = true, path = path, instanceCount = count }
|
|
245
527
|
end
|
|
246
528
|
|
|
529
|
+
--[[
|
|
530
|
+
Serialize: convert an existing instance tree into create_ui-compatible JSON.
|
|
531
|
+
Reads properties via ReflectionService, converts Roblox types to JSON-friendly values,
|
|
532
|
+
and skips default/unchanged values to keep output minimal.
|
|
533
|
+
]]
|
|
534
|
+
|
|
535
|
+
local Reflection = require(script.Parent.Reflection)
|
|
536
|
+
local ReflectionService = game:GetService("ReflectionService")
|
|
537
|
+
|
|
538
|
+
-- Convert a Roblox value to a JSON-friendly representation
|
|
539
|
+
local function valueToJson(value, propType)
|
|
540
|
+
local t = typeof(value)
|
|
541
|
+
|
|
542
|
+
if t == "UDim2" then
|
|
543
|
+
return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
|
|
544
|
+
end
|
|
545
|
+
if t == "UDim" then
|
|
546
|
+
return { value.Scale, value.Offset }
|
|
547
|
+
end
|
|
548
|
+
if t == "Vector2" then
|
|
549
|
+
return { value.X, value.Y }
|
|
550
|
+
end
|
|
551
|
+
if t == "Vector3" then
|
|
552
|
+
return { value.X, value.Y, value.Z }
|
|
553
|
+
end
|
|
554
|
+
if t == "Color3" then
|
|
555
|
+
return "#" .. value:ToHex()
|
|
556
|
+
end
|
|
557
|
+
if t == "BrickColor" then
|
|
558
|
+
return value.Name
|
|
559
|
+
end
|
|
560
|
+
if t == "ColorSequence" then
|
|
561
|
+
local kps = {}
|
|
562
|
+
for _, kp in value.Keypoints do
|
|
563
|
+
table.insert(kps, { kp.Time, "#" .. kp.Value:ToHex() })
|
|
564
|
+
end
|
|
565
|
+
return kps
|
|
566
|
+
end
|
|
567
|
+
if t == "NumberSequence" then
|
|
568
|
+
local kps = {}
|
|
569
|
+
for _, kp in value.Keypoints do
|
|
570
|
+
table.insert(kps, { kp.Time, kp.Value, kp.Envelope })
|
|
571
|
+
end
|
|
572
|
+
return kps
|
|
573
|
+
end
|
|
574
|
+
if t == "NumberRange" then
|
|
575
|
+
return { value.Min, value.Max }
|
|
576
|
+
end
|
|
577
|
+
if t == "Rect" then
|
|
578
|
+
return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y }
|
|
579
|
+
end
|
|
580
|
+
if t == "CFrame" then
|
|
581
|
+
return { value:GetComponents() }
|
|
582
|
+
end
|
|
583
|
+
if t == "EnumItem" then
|
|
584
|
+
return value.Name
|
|
585
|
+
end
|
|
586
|
+
if t == "Font" then
|
|
587
|
+
return {
|
|
588
|
+
Family = value.Family,
|
|
589
|
+
Weight = value.Weight.Name,
|
|
590
|
+
Style = value.Style.Name,
|
|
591
|
+
}
|
|
592
|
+
end
|
|
593
|
+
if t == "boolean" or t == "number" or t == "string" then
|
|
594
|
+
return value
|
|
595
|
+
end
|
|
596
|
+
-- Fallback: tostring
|
|
597
|
+
return tostring(value)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
local SERIALIZE_SKIP = {
|
|
601
|
+
Parent = true,
|
|
602
|
+
ClassName = true,
|
|
603
|
+
Name = true,
|
|
604
|
+
Archivable = true,
|
|
605
|
+
AbsolutePosition = true,
|
|
606
|
+
AbsoluteSize = true,
|
|
607
|
+
AbsoluteRotation = true,
|
|
608
|
+
IsLoaded = true,
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
local function serializeNode(instance, maxDepth, depth)
|
|
612
|
+
if depth > maxDepth then
|
|
613
|
+
return nil
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
local node = {
|
|
617
|
+
ClassName = instance.ClassName,
|
|
618
|
+
Name = instance.Name,
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
-- Get non-default properties via reflection
|
|
622
|
+
local ok, propData = pcall(function()
|
|
623
|
+
return ReflectionService:GetPropertiesOfClass(instance.ClassName)
|
|
624
|
+
end)
|
|
625
|
+
|
|
626
|
+
if ok and propData then
|
|
627
|
+
-- Create a default instance to compare against
|
|
628
|
+
local defaultInstance = nil
|
|
629
|
+
pcall(function()
|
|
630
|
+
defaultInstance = Instance.new(instance.ClassName)
|
|
631
|
+
end)
|
|
632
|
+
|
|
633
|
+
for _, prop in propData do
|
|
634
|
+
if not SERIALIZE_SKIP[prop.Name] and not prop.Name:match("^Absolute") then
|
|
635
|
+
local valOk, value = pcall(function()
|
|
636
|
+
return instance[prop.Name]
|
|
637
|
+
end)
|
|
638
|
+
if valOk and value ~= nil then
|
|
639
|
+
-- Skip default values
|
|
640
|
+
local isDefault = false
|
|
641
|
+
if defaultInstance then
|
|
642
|
+
local defOk, defVal = pcall(function()
|
|
643
|
+
return defaultInstance[prop.Name]
|
|
644
|
+
end)
|
|
645
|
+
if defOk and defVal == value then
|
|
646
|
+
isDefault = true
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
if not isDefault then
|
|
651
|
+
local jsonVal = valueToJson(value, prop.ValueType and tostring(prop.ValueType))
|
|
652
|
+
if jsonVal ~= nil then
|
|
653
|
+
node[prop.Name] = jsonVal
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
if defaultInstance then
|
|
661
|
+
pcall(function()
|
|
662
|
+
defaultInstance:Destroy()
|
|
663
|
+
end)
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
-- Serialize children
|
|
668
|
+
local children = instance:GetChildren()
|
|
669
|
+
if #children > 0 then
|
|
670
|
+
node.Children = {}
|
|
671
|
+
for _, child in children do
|
|
672
|
+
local childNode = serializeNode(child, maxDepth, depth + 1)
|
|
673
|
+
if childNode then
|
|
674
|
+
table.insert(node.Children, childNode)
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
if #node.Children == 0 then
|
|
678
|
+
node.Children = nil
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
return node
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
function UIBuilder.serialize(spec)
|
|
686
|
+
local path = spec.path
|
|
687
|
+
if not path then
|
|
688
|
+
return { success = false, error = "Missing 'path' parameter" }
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
local instance = Explorer.resolveInstance(path)
|
|
692
|
+
if not instance then
|
|
693
|
+
return { success = false, error = "Instance not found: " .. path }
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
local maxDepth = spec.maxDepth or 50
|
|
697
|
+
|
|
698
|
+
local ok, tree = pcall(function()
|
|
699
|
+
return serializeNode(instance, maxDepth, 0)
|
|
700
|
+
end)
|
|
701
|
+
|
|
702
|
+
if not ok then
|
|
703
|
+
return { success = false, error = "Serialization failed: " .. tostring(tree) }
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
return { success = true, tree = tree }
|
|
707
|
+
end
|
|
708
|
+
|
|
247
709
|
return UIBuilder
|