@el4cteo/rbx-studio-mcp 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/dist/bridge/console.js +182 -0
- package/dist/bridge/console.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/cloudassets.js +233 -0
- package/dist/lib/cloudassets.js.map +1 -0
- package/dist/lib/credentials.js +180 -0
- package/dist/lib/credentials.js.map +1 -0
- package/dist/lib/livedata.js +325 -0
- package/dist/lib/livedata.js.map +1 -0
- package/dist/lib/liveluau.js +83 -0
- package/dist/lib/liveluau.js.map +1 -0
- package/dist/lib/liveops.js +358 -0
- package/dist/lib/liveops.js.map +1 -0
- package/dist/lib/opencloud.js +235 -0
- package/dist/lib/opencloud.js.map +1 -0
- package/dist/tools/audio.js +96 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/data.js +122 -3
- package/dist/tools/data.js.map +1 -1
- package/dist/tools/exec.js +48 -1
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/scripts.js +112 -4
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/spatial.js +135 -0
- package/dist/tools/spatial.js.map +1 -0
- package/dist/tools/universe.js +177 -0
- package/dist/tools/universe.js.map +1 -0
- package/dist/tools/upload.js +294 -0
- package/dist/tools/upload.js.map +1 -0
- package/dist/tools/world.js +361 -10
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Commands.luau +646 -622
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Phrase.luau +816 -766
- package/plugin/src/Prompt.luau +965 -961
- package/plugin/src/Secret.luau +86 -0
- package/plugin/src/Serialize.luau +759 -499
- package/plugin/src/handlers/Assets.luau +636 -587
- package/plugin/src/handlers/Audio.luau +411 -0
- package/plugin/src/handlers/Geometry.luau +722 -577
- package/plugin/src/handlers/Instances.luau +84 -4
- package/plugin/src/handlers/Spatial.luau +334 -0
- package/plugin/src/init.server.luau +883 -879
|
@@ -1,499 +1,759 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
Converts Roblox values into JSON-safe, token-cheap representations.
|
|
4
|
-
|
|
5
|
-
Format mirrors what the Studio Properties panel shows -- Vector3 as
|
|
6
|
-
"12, 0, 5", Enums as "Enum.Material.Plastic", instance references as their
|
|
7
|
-
path. Two reasons: a Roblox developer reading the transcript sees what they
|
|
8
|
-
would see in Studio, and a model has seen far more of that notation in
|
|
9
|
-
training than any bespoke JSON envelope. It is also markedly cheaper than
|
|
10
|
-
{"__type":"Vector3","x":12,"y":0,"z":5} on every property of every instance.
|
|
11
|
-
|
|
12
|
-
`Serialize.parse` is the exact inverse, so a value read here can be written
|
|
13
|
-
straight back without the agent reformatting it.
|
|
14
|
-
]]
|
|
15
|
-
|
|
16
|
-
local Paths = require(script.Parent.Paths)
|
|
17
|
-
|
|
18
|
-
local Serialize = {}
|
|
19
|
-
|
|
20
|
-
--
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
local
|
|
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
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return string.format(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return string.format(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
return
|
|
151
|
-
end
|
|
152
|
-
if kind == "
|
|
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
|
-
|
|
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
|
-
if
|
|
229
|
-
|
|
230
|
-
end
|
|
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
|
-
local
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
end
|
|
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
|
-
if
|
|
326
|
-
return true,
|
|
327
|
-
end
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if
|
|
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
|
-
return
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
end
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Converts Roblox values into JSON-safe, token-cheap representations.
|
|
4
|
+
|
|
5
|
+
Format mirrors what the Studio Properties panel shows -- Vector3 as
|
|
6
|
+
"12, 0, 5", Enums as "Enum.Material.Plastic", instance references as their
|
|
7
|
+
path. Two reasons: a Roblox developer reading the transcript sees what they
|
|
8
|
+
would see in Studio, and a model has seen far more of that notation in
|
|
9
|
+
training than any bespoke JSON envelope. It is also markedly cheaper than
|
|
10
|
+
{"__type":"Vector3","x":12,"y":0,"z":5} on every property of every instance.
|
|
11
|
+
|
|
12
|
+
`Serialize.parse` is the exact inverse, so a value read here can be written
|
|
13
|
+
straight back without the agent reformatting it.
|
|
14
|
+
]]
|
|
15
|
+
|
|
16
|
+
local Paths = require(script.Parent.Paths)
|
|
17
|
+
|
|
18
|
+
local Serialize = {}
|
|
19
|
+
|
|
20
|
+
--[[
|
|
21
|
+
Marks a failure that means "the instance is not there" rather than "this
|
|
22
|
+
value is wrong".
|
|
23
|
+
|
|
24
|
+
The create handler retries only these, once the whole batch is attached, so
|
|
25
|
+
a reference to a sibling made in the same call resolves. Anything else is a
|
|
26
|
+
real mistake and is reported straight away.
|
|
27
|
+
]]
|
|
28
|
+
local UNRESOLVED_PREFIX = "[unresolved] "
|
|
29
|
+
Serialize.UNRESOLVED_PREFIX = UNRESOLVED_PREFIX
|
|
30
|
+
|
|
31
|
+
-- Floats are rounded before display: Studio reports positions like
|
|
32
|
+
-- 12.000000476837158, which costs tokens and means nothing to the caller.
|
|
33
|
+
local PRECISION = 4
|
|
34
|
+
|
|
35
|
+
-- API-dump type names that accept a plain Luau number.
|
|
36
|
+
local NUMERIC_TYPES: { [string]: boolean } = {
|
|
37
|
+
int = true,
|
|
38
|
+
int64 = true,
|
|
39
|
+
float = true,
|
|
40
|
+
double = true,
|
|
41
|
+
number = true,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
local function num(value: number): string
|
|
45
|
+
local rounded = math.round(value * 10 ^ PRECISION) / 10 ^ PRECISION
|
|
46
|
+
if rounded == math.floor(rounded) and math.abs(rounded) < 1e15 then
|
|
47
|
+
return string.format("%d", rounded)
|
|
48
|
+
end
|
|
49
|
+
return tostring(rounded)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
--[[
|
|
53
|
+
Serializes one value. Unknown userdata falls back to `tostring`, which is
|
|
54
|
+
always better than dropping the property silently.
|
|
55
|
+
]]
|
|
56
|
+
function Serialize.value(value: any): any
|
|
57
|
+
local kind = typeof(value)
|
|
58
|
+
|
|
59
|
+
if kind == "string" or kind == "boolean" or kind == "nil" then
|
|
60
|
+
return value
|
|
61
|
+
end
|
|
62
|
+
if kind == "number" then
|
|
63
|
+
-- Kept numeric so the agent can do arithmetic without parsing.
|
|
64
|
+
return math.round(value * 10 ^ PRECISION) / 10 ^ PRECISION
|
|
65
|
+
end
|
|
66
|
+
if kind == "Vector3" then
|
|
67
|
+
return string.format("%s, %s, %s", num(value.X), num(value.Y), num(value.Z))
|
|
68
|
+
end
|
|
69
|
+
if kind == "Vector2" then
|
|
70
|
+
return string.format("%s, %s", num(value.X), num(value.Y))
|
|
71
|
+
end
|
|
72
|
+
if kind == "CFrame" then
|
|
73
|
+
local position = value.Position
|
|
74
|
+
local rx, ry, rz = value:ToOrientation()
|
|
75
|
+
return string.format(
|
|
76
|
+
"pos %s, %s, %s | rot %s, %s, %s",
|
|
77
|
+
num(position.X),
|
|
78
|
+
num(position.Y),
|
|
79
|
+
num(position.Z),
|
|
80
|
+
num(math.deg(rx)),
|
|
81
|
+
num(math.deg(ry)),
|
|
82
|
+
num(math.deg(rz))
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
if kind == "Color3" then
|
|
86
|
+
return string.format("%s, %s, %s", num(value.R), num(value.G), num(value.B))
|
|
87
|
+
end
|
|
88
|
+
if kind == "BrickColor" then
|
|
89
|
+
return value.Name
|
|
90
|
+
end
|
|
91
|
+
if kind == "UDim2" then
|
|
92
|
+
return string.format(
|
|
93
|
+
"{%s, %s}, {%s, %s}",
|
|
94
|
+
num(value.X.Scale),
|
|
95
|
+
num(value.X.Offset),
|
|
96
|
+
num(value.Y.Scale),
|
|
97
|
+
num(value.Y.Offset)
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
if kind == "UDim" then
|
|
101
|
+
return string.format("{%s, %s}", num(value.Scale), num(value.Offset))
|
|
102
|
+
end
|
|
103
|
+
if kind == "Font" then
|
|
104
|
+
--[[
|
|
105
|
+
Written so it can be read back, which `tostring` is not.
|
|
106
|
+
|
|
107
|
+
A Font stringifies as `Font { Family = rbxasset://..., Weight = Bold,
|
|
108
|
+
Style = Normal }`, which nothing here can parse. The family, weight
|
|
109
|
+
and style are all there is to one, so they are written plainly and
|
|
110
|
+
separated the way a composite value is elsewhere in this module.
|
|
111
|
+
]]
|
|
112
|
+
return string.format(
|
|
113
|
+
"%s | %s | %s",
|
|
114
|
+
value.Family,
|
|
115
|
+
value.Weight.Name,
|
|
116
|
+
value.Style.Name
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
if kind == "EnumItem" then
|
|
120
|
+
return string.format("Enum.%s.%s", tostring(value.EnumType), value.Name)
|
|
121
|
+
end
|
|
122
|
+
if kind == "Content" then
|
|
123
|
+
--[[
|
|
124
|
+
Written as the bare URI so it can be read back.
|
|
125
|
+
|
|
126
|
+
`tostring` on a Content gives `Content{SourceType=Uri,
|
|
127
|
+
Uri=rbxassetid://123}`, which `parse` cannot take. A Content that
|
|
128
|
+
holds an Object or no source at all has no URI to give, so those are
|
|
129
|
+
named rather than rendered as an empty string that would read as
|
|
130
|
+
"cleared".
|
|
131
|
+
]]
|
|
132
|
+
if value.Uri then
|
|
133
|
+
return value.Uri
|
|
134
|
+
end
|
|
135
|
+
if value.Object then
|
|
136
|
+
return Paths.of(value.Object)
|
|
137
|
+
end
|
|
138
|
+
return ""
|
|
139
|
+
end
|
|
140
|
+
if kind == "Instance" then
|
|
141
|
+
--[[
|
|
142
|
+
Formatted as an address, not as a display name.
|
|
143
|
+
|
|
144
|
+
`GetFullName` writes "StarterPack.Pistol.Handle" for both of two
|
|
145
|
+
Tools named Pistol, so reading a Motor6D's Part1 and writing it back
|
|
146
|
+
pointed at the wrong one -- silently, since both paths resolve.
|
|
147
|
+
`Paths.of` adds the [n] that tells them apart, which is what makes a
|
|
148
|
+
value read here safe to write straight back.
|
|
149
|
+
]]
|
|
150
|
+
return Paths.of(value)
|
|
151
|
+
end
|
|
152
|
+
if kind == "NumberRange" then
|
|
153
|
+
return string.format("%s..%s", num(value.Min), num(value.Max))
|
|
154
|
+
end
|
|
155
|
+
if kind == "Rect" then
|
|
156
|
+
return string.format(
|
|
157
|
+
"%s, %s, %s, %s",
|
|
158
|
+
num(value.Min.X),
|
|
159
|
+
num(value.Min.Y),
|
|
160
|
+
num(value.Max.X),
|
|
161
|
+
num(value.Max.Y)
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
if kind == "ColorSequence" then
|
|
165
|
+
local parts: { string } = {}
|
|
166
|
+
for _, keypoint in value.Keypoints do
|
|
167
|
+
table.insert(
|
|
168
|
+
parts,
|
|
169
|
+
string.format("%s=%s", num(keypoint.Time), Serialize.value(keypoint.Value))
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
return table.concat(parts, " ")
|
|
173
|
+
end
|
|
174
|
+
if kind == "NumberSequence" then
|
|
175
|
+
local parts: { string } = {}
|
|
176
|
+
for _, keypoint in value.Keypoints do
|
|
177
|
+
table.insert(parts, string.format("%s=%s", num(keypoint.Time), num(keypoint.Value)))
|
|
178
|
+
end
|
|
179
|
+
return table.concat(parts, " ")
|
|
180
|
+
end
|
|
181
|
+
if kind == "table" then
|
|
182
|
+
local copy: { [any]: any } = {}
|
|
183
|
+
for key, item in value :: { [any]: any } do
|
|
184
|
+
copy[key] = Serialize.value(item)
|
|
185
|
+
end
|
|
186
|
+
return copy
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
return tostring(value)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
--[[
|
|
193
|
+
Reads a property without throwing.
|
|
194
|
+
|
|
195
|
+
Property access on an instance can error even when the API dump says the
|
|
196
|
+
property exists -- some are context-gated (only readable during a playtest,
|
|
197
|
+
or only on a loaded asset). Returning nil for those keeps one awkward
|
|
198
|
+
property from failing an inspect of 50 instances.
|
|
199
|
+
]]
|
|
200
|
+
function Serialize.readProperty(instance: Instance, name: string): (boolean, any)
|
|
201
|
+
local ok, value = pcall(function()
|
|
202
|
+
return (instance :: any)[name]
|
|
203
|
+
end)
|
|
204
|
+
if not ok then
|
|
205
|
+
return false, nil
|
|
206
|
+
end
|
|
207
|
+
return true, Serialize.value(value)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
--[[
|
|
211
|
+
Pulls the numbers out of a composite value written as text.
|
|
212
|
+
|
|
213
|
+
This used to be a single `-?%d+%.?%d*` scan, which is not the grammar Luau
|
|
214
|
+
numbers actually have: "1e3, 0, .5" came back as 1, 3 and 0, so a Position
|
|
215
|
+
written in scientific notation was silently set to a different point in the
|
|
216
|
+
world and reported as applied. Splitting on the separators and handing each
|
|
217
|
+
token to `tonumber` means this agrees with the scalar path, which always
|
|
218
|
+
used `tonumber` -- the two disagreeing about what counts as a number was the
|
|
219
|
+
whole bug.
|
|
220
|
+
|
|
221
|
+
Non-numeric tokens are dropped, so "Vector3.new(1, 2, 3)" and "{1, 2, 3}"
|
|
222
|
+
still read as three numbers.
|
|
223
|
+
]]
|
|
224
|
+
local function scanTokens(text: string): { number }
|
|
225
|
+
local numbers: { number } = {}
|
|
226
|
+
for token in string.gmatch(text, "[^,%s%(%)%[%]{}<>]+") do
|
|
227
|
+
local value = tonumber(token)
|
|
228
|
+
if value then
|
|
229
|
+
table.insert(numbers, value)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
return numbers
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
--[[
|
|
236
|
+
ColorSequence and NumberSequence.
|
|
237
|
+
|
|
238
|
+
`Serialize.value` has always WRITTEN these -- "0=1, 0, 0 1=0, 0, 1" for a
|
|
239
|
+
red-to-blue gradient -- and `parse` had no branch for either, so every one of
|
|
240
|
+
them could be read and none could be written back. That is not a corner:
|
|
241
|
+
ColorSequence and NumberSequence are what ParticleEmitter, Beam, Trail and
|
|
242
|
+
UIGradient are made of, so recolouring any effect fell out of `modify`
|
|
243
|
+
entirely and needed `execute_luau`. Found by auditing the round trip rather
|
|
244
|
+
than by hitting it.
|
|
245
|
+
|
|
246
|
+
Keypoints are split on the "<time>=" markers rather than on whitespace,
|
|
247
|
+
because the VALUE of a ColorSequence keypoint contains both commas and
|
|
248
|
+
spaces of its own and nothing else can tell the two apart.
|
|
249
|
+
|
|
250
|
+
A bare value is also accepted and means a constant sequence: `Color3` text
|
|
251
|
+
for a ColorSequence, one number for a NumberSequence. People write
|
|
252
|
+
`Color = "1, 0, 0"` meaning solid red, and refusing that to insist on
|
|
253
|
+
"0=1, 0, 0 1=1, 0, 0" would be pedantry.
|
|
254
|
+
]]
|
|
255
|
+
local function splitKeypoints(text: string): { { time: number, value: string } }
|
|
256
|
+
local marks: { { at: number, finish: number, time: number } } = {}
|
|
257
|
+
local index = 1
|
|
258
|
+
while true do
|
|
259
|
+
local at, finish, captured = string.find(text, "([%d%.]+)%s*=", index)
|
|
260
|
+
if not at then
|
|
261
|
+
break
|
|
262
|
+
end
|
|
263
|
+
local time = tonumber(captured)
|
|
264
|
+
if time then
|
|
265
|
+
table.insert(marks, { at = at, finish = finish, time = time })
|
|
266
|
+
end
|
|
267
|
+
index = finish + 1
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
local out: { { time: number, value: string } } = {}
|
|
271
|
+
for position, mark in marks do
|
|
272
|
+
local stop = if marks[position + 1] then marks[position + 1].at - 1 else #text
|
|
273
|
+
local value = string.match(string.sub(text, mark.finish + 1, stop), "^%s*(.-)%s*$") or ""
|
|
274
|
+
table.insert(out, { time = mark.time, value = value })
|
|
275
|
+
end
|
|
276
|
+
return out
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
--[[
|
|
281
|
+
Parses a serialized string back into a Roblox value, given the target type
|
|
282
|
+
from the API dump. Returns ok=false with a reason the agent can act on
|
|
283
|
+
rather than raising, so batch writes can report per-property failures.
|
|
284
|
+
]]
|
|
285
|
+
function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
286
|
+
local kind = typeof(text)
|
|
287
|
+
|
|
288
|
+
-- Values already of the right primitive shape pass straight through. The
|
|
289
|
+
-- number case is matched against the target type rather than excluded from
|
|
290
|
+
-- one: a bare 5 is a valid `float`, but not a valid Vector3, Enum or Color3.
|
|
291
|
+
if kind == "boolean" then
|
|
292
|
+
return true, text, nil
|
|
293
|
+
end
|
|
294
|
+
if kind == "number" and NUMERIC_TYPES[valueType] then
|
|
295
|
+
return true, text, nil
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
if valueType == "string" or valueType == "ProtectedString" then
|
|
299
|
+
return true, tostring(text), nil
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
--[[
|
|
303
|
+
Asset references come in two shapes with almost the same name.
|
|
304
|
+
|
|
305
|
+
`ContentId` is the old one and is still just a string: `MeshPart.MeshId`,
|
|
306
|
+
`Decal.Texture`, `SurfaceAppearance.ColorMap`. It had no branch here at
|
|
307
|
+
all, so every one of them failed with `no conversion known for type
|
|
308
|
+
"ContentId"` -- which is how a SurfaceAppearance could be created but
|
|
309
|
+
never given a texture.
|
|
310
|
+
|
|
311
|
+
`Content` is the new datatype and is NOT a string. Assigning
|
|
312
|
+
`"rbxassetid://123"` to `AudioPlayer.AudioContent` or
|
|
313
|
+
`ImageLabel.ImageContent` throws; it wants `Content.fromUri`. This branch
|
|
314
|
+
used to lump it in with `string` above, so those writes reported a parse
|
|
315
|
+
success and then failed at assignment with a message about a type the
|
|
316
|
+
caller never mentioned.
|
|
317
|
+
|
|
318
|
+
An empty string means "nothing", the same as it does for an instance
|
|
319
|
+
reference: `Content.none` is the engine's own name for that.
|
|
320
|
+
]]
|
|
321
|
+
if valueType == "ContentId" then
|
|
322
|
+
return true, tostring(text), nil
|
|
323
|
+
end
|
|
324
|
+
if valueType == "Content" then
|
|
325
|
+
if typeof(text) == "Content" then
|
|
326
|
+
return true, text, nil
|
|
327
|
+
end
|
|
328
|
+
local uri = tostring(text)
|
|
329
|
+
if uri == "" then
|
|
330
|
+
return true, Content.none, nil
|
|
331
|
+
end
|
|
332
|
+
local okContent, content = pcall(function()
|
|
333
|
+
return Content.fromUri(uri)
|
|
334
|
+
end)
|
|
335
|
+
if okContent then
|
|
336
|
+
return true, content, nil
|
|
337
|
+
end
|
|
338
|
+
return false, nil, string.format('"%s" is not a content URI', uri)
|
|
339
|
+
end
|
|
340
|
+
if valueType == "bool" then
|
|
341
|
+
if kind == "boolean" then
|
|
342
|
+
return true, text, nil
|
|
343
|
+
end
|
|
344
|
+
if kind == "number" then
|
|
345
|
+
return true, text ~= 0, nil
|
|
346
|
+
end
|
|
347
|
+
-- This used to be `text == "true"`, which quietly turned "True", "TRUE"
|
|
348
|
+
-- and "1" into false and reported the write as applied. A bool that is
|
|
349
|
+
-- silently the opposite of what was asked is the worst failure this
|
|
350
|
+
-- module can produce, so near misses are accepted and anything else is
|
|
351
|
+
-- refused rather than guessed at.
|
|
352
|
+
local lowered = string.lower(tostring(text))
|
|
353
|
+
if lowered == "true" or lowered == "1" or lowered == "yes" then
|
|
354
|
+
return true, true, nil
|
|
355
|
+
end
|
|
356
|
+
if lowered == "false" or lowered == "0" or lowered == "no" then
|
|
357
|
+
return true, false, nil
|
|
358
|
+
end
|
|
359
|
+
return false, nil, string.format('expected true or false, got "%s"', tostring(text))
|
|
360
|
+
end
|
|
361
|
+
if NUMERIC_TYPES[valueType] then
|
|
362
|
+
local parsed = tonumber(text)
|
|
363
|
+
if not parsed then
|
|
364
|
+
return false, nil, string.format('expected a number, got "%s"', tostring(text))
|
|
365
|
+
end
|
|
366
|
+
return true, parsed, nil
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
local numbers: { number } = {}
|
|
370
|
+
if kind == "string" then
|
|
371
|
+
for _, token in scanTokens(text) do
|
|
372
|
+
table.insert(numbers, token)
|
|
373
|
+
end
|
|
374
|
+
elseif kind == "table" then
|
|
375
|
+
for _, item in text :: { any } do
|
|
376
|
+
local parsed = tonumber(item)
|
|
377
|
+
if parsed then
|
|
378
|
+
table.insert(numbers, parsed)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
if valueType == "Vector3" then
|
|
384
|
+
if #numbers < 3 then
|
|
385
|
+
return false, nil, 'expected three numbers, e.g. "12, 0, 5"'
|
|
386
|
+
end
|
|
387
|
+
return true, Vector3.new(numbers[1], numbers[2], numbers[3]), nil
|
|
388
|
+
end
|
|
389
|
+
if valueType == "Vector2" then
|
|
390
|
+
if #numbers < 2 then
|
|
391
|
+
return false, nil, 'expected two numbers, e.g. "12, 5"'
|
|
392
|
+
end
|
|
393
|
+
return true, Vector2.new(numbers[1], numbers[2]), nil
|
|
394
|
+
end
|
|
395
|
+
if valueType == "Color3" then
|
|
396
|
+
if #numbers < 3 then
|
|
397
|
+
return false, nil, 'expected three 0-1 components, e.g. "1, 0.5, 0"'
|
|
398
|
+
end
|
|
399
|
+
-- 0-255 is a common mistake and unambiguous to detect, so accept it.
|
|
400
|
+
local scale = if numbers[1] > 1 or numbers[2] > 1 or numbers[3] > 1 then 255 else 1
|
|
401
|
+
return true, Color3.new(numbers[1] / scale, numbers[2] / scale, numbers[3] / scale), nil
|
|
402
|
+
end
|
|
403
|
+
if valueType == "BrickColor" then
|
|
404
|
+
-- Cast because the API definitions type this overload as a literal union
|
|
405
|
+
-- of every BrickColor name. At runtime it takes any string and throws on
|
|
406
|
+
-- an unknown one, which is exactly what the pcall is here to catch.
|
|
407
|
+
local ok, color = pcall(function()
|
|
408
|
+
return BrickColor.new(tostring(text) :: any)
|
|
409
|
+
end)
|
|
410
|
+
if not ok then
|
|
411
|
+
return false, nil, string.format('"%s" is not a BrickColor name', tostring(text))
|
|
412
|
+
end
|
|
413
|
+
return true, color, nil
|
|
414
|
+
end
|
|
415
|
+
if valueType == "UDim2" then
|
|
416
|
+
if #numbers < 4 then
|
|
417
|
+
return false, nil, 'expected four numbers, e.g. "{0.5, 10}, {0.5, 20}"'
|
|
418
|
+
end
|
|
419
|
+
return true, UDim2.new(numbers[1], numbers[2], numbers[3], numbers[4]), nil
|
|
420
|
+
end
|
|
421
|
+
if valueType == "UDim" then
|
|
422
|
+
if #numbers < 2 then
|
|
423
|
+
return false, nil, 'expected two numbers, e.g. "{0.5, 10}"'
|
|
424
|
+
end
|
|
425
|
+
return true, UDim.new(numbers[1], numbers[2]), nil
|
|
426
|
+
end
|
|
427
|
+
if valueType == "CFrame" then
|
|
428
|
+
if #numbers >= 6 then
|
|
429
|
+
return true,
|
|
430
|
+
CFrame.new(numbers[1], numbers[2], numbers[3])
|
|
431
|
+
* CFrame.fromOrientation(
|
|
432
|
+
math.rad(numbers[4]),
|
|
433
|
+
math.rad(numbers[5]),
|
|
434
|
+
math.rad(numbers[6])
|
|
435
|
+
),
|
|
436
|
+
nil
|
|
437
|
+
end
|
|
438
|
+
if #numbers >= 3 then
|
|
439
|
+
return true, CFrame.new(numbers[1], numbers[2], numbers[3]), nil
|
|
440
|
+
end
|
|
441
|
+
return false, nil, 'expected "pos x, y, z" or "pos x, y, z | rot rx, ry, rz"'
|
|
442
|
+
end
|
|
443
|
+
--[[
|
|
444
|
+
NumberRange, which the shared tokenizer cannot see.
|
|
445
|
+
|
|
446
|
+
`Serialize.value` writes one as "1..3", but `scanTokens` splits on
|
|
447
|
+
commas, whitespace and brackets -- never on "..", so "1..3" arrives as a
|
|
448
|
+
single token, `tonumber` returns nil, and the parse failed with `expected
|
|
449
|
+
"min..max"` while being handed exactly that. A value this module printed
|
|
450
|
+
could not be fed back to it, which is the one promise the whole file
|
|
451
|
+
makes. Caught in QA on ParticleEmitter.Lifetime.
|
|
452
|
+
|
|
453
|
+
Parsed here directly. A single number is accepted too, because
|
|
454
|
+
`NumberRange.new(2)` is a real range and people write it that way.
|
|
455
|
+
]]
|
|
456
|
+
if valueType == "ColorSequence" then
|
|
457
|
+
if kind ~= "string" then
|
|
458
|
+
return false, nil, "expected text like \"0=1, 0, 0 1=0, 0, 1\""
|
|
459
|
+
end
|
|
460
|
+
local points = splitKeypoints(text)
|
|
461
|
+
if #points == 0 then
|
|
462
|
+
-- A bare colour: a sequence that is that colour the whole way.
|
|
463
|
+
local okColour, colour = Serialize.parse(text, "Color3")
|
|
464
|
+
if okColour and typeof(colour) == "Color3" then
|
|
465
|
+
return true, ColorSequence.new(colour :: Color3), nil
|
|
466
|
+
end
|
|
467
|
+
return false, nil, "expected \"0=1, 0, 0 1=0, 0, 1\", or one colour for a solid sequence"
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
local keypoints: { ColorSequenceKeypoint } = {}
|
|
471
|
+
for _, point in points do
|
|
472
|
+
local okColour, colour = Serialize.parse(point.value, "Color3")
|
|
473
|
+
if not okColour or typeof(colour) ~= "Color3" then
|
|
474
|
+
return false, nil, string.format("keypoint at %s is not a colour: %q", tostring(point.time), point.value)
|
|
475
|
+
end
|
|
476
|
+
table.insert(keypoints, ColorSequenceKeypoint.new(point.time, colour :: Color3))
|
|
477
|
+
end
|
|
478
|
+
-- Roblox requires the first keypoint at 0 and the last at 1, in order, and
|
|
479
|
+
-- throws an unhelpful error otherwise.
|
|
480
|
+
table.sort(keypoints, function(a, b)
|
|
481
|
+
return a.Time < b.Time
|
|
482
|
+
end)
|
|
483
|
+
if keypoints[1].Time ~= 0 or keypoints[#keypoints].Time ~= 1 then
|
|
484
|
+
return false,
|
|
485
|
+
nil,
|
|
486
|
+
"a ColorSequence must start at time 0 and end at time 1"
|
|
487
|
+
end
|
|
488
|
+
local okBuild, built = pcall(ColorSequence.new, keypoints)
|
|
489
|
+
if not okBuild then
|
|
490
|
+
return false, nil, tostring(built)
|
|
491
|
+
end
|
|
492
|
+
return true, built, nil
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
if valueType == "NumberSequence" then
|
|
496
|
+
if kind == "number" then
|
|
497
|
+
return true, NumberSequence.new(text), nil
|
|
498
|
+
end
|
|
499
|
+
if kind ~= "string" then
|
|
500
|
+
return false, nil, "expected text like \"0=0 1=1\""
|
|
501
|
+
end
|
|
502
|
+
local points = splitKeypoints(text)
|
|
503
|
+
if #points == 0 then
|
|
504
|
+
local single = tonumber(text)
|
|
505
|
+
if single then
|
|
506
|
+
return true, NumberSequence.new(single), nil
|
|
507
|
+
end
|
|
508
|
+
return false, nil, "expected \"0=0 1=1\", or one number for a flat sequence"
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
local keypoints: { NumberSequenceKeypoint } = {}
|
|
512
|
+
for _, point in points do
|
|
513
|
+
local value = tonumber(point.value)
|
|
514
|
+
if not value then
|
|
515
|
+
return false, nil, string.format("keypoint at %s is not a number: %q", tostring(point.time), point.value)
|
|
516
|
+
end
|
|
517
|
+
table.insert(keypoints, NumberSequenceKeypoint.new(point.time, value))
|
|
518
|
+
end
|
|
519
|
+
table.sort(keypoints, function(a, b)
|
|
520
|
+
return a.Time < b.Time
|
|
521
|
+
end)
|
|
522
|
+
if keypoints[1].Time ~= 0 or keypoints[#keypoints].Time ~= 1 then
|
|
523
|
+
return false, nil, "a NumberSequence must start at time 0 and end at time 1"
|
|
524
|
+
end
|
|
525
|
+
local okBuild, built = pcall(NumberSequence.new, keypoints)
|
|
526
|
+
if not okBuild then
|
|
527
|
+
return false, nil, tostring(built)
|
|
528
|
+
end
|
|
529
|
+
return true, built, nil
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
--[[
|
|
533
|
+
Rect, which `Serialize.value` writes and `parse` did not read.
|
|
534
|
+
|
|
535
|
+
The last of the write-but-cannot-read set, after NumberRange and the two
|
|
536
|
+
sequences. Offered in the attribute type list too, so `rect` attributes
|
|
537
|
+
were advertised and refused in the same breath. Four numbers, in the
|
|
538
|
+
order the writer emits them: min X, min Y, max X, max Y.
|
|
539
|
+
]]
|
|
540
|
+
if valueType == "Rect" then
|
|
541
|
+
if #numbers < 4 then
|
|
542
|
+
return false, nil, 'expected "minX, minY, maxX, maxY"'
|
|
543
|
+
end
|
|
544
|
+
return true, Rect.new(numbers[1], numbers[2], numbers[3], numbers[4]), nil
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
if valueType == "NumberRange" then
|
|
548
|
+
if kind == "string" then
|
|
549
|
+
local low, high = string.match(text, "^%s*(-?[%d%.]+)%s*%.%.%s*(-?[%d%.]+)%s*$")
|
|
550
|
+
if low and high and tonumber(low) and tonumber(high) then
|
|
551
|
+
return true, NumberRange.new(tonumber(low) :: number, tonumber(high) :: number), nil
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
if #numbers >= 2 then
|
|
555
|
+
return true, NumberRange.new(numbers[1], numbers[2]), nil
|
|
556
|
+
end
|
|
557
|
+
if #numbers == 1 then
|
|
558
|
+
return true, NumberRange.new(numbers[1]), nil
|
|
559
|
+
end
|
|
560
|
+
return false, nil, 'expected "min..max", e.g. "1..3"'
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
--[[
|
|
564
|
+
PhysicalProperties, which is neither a vector nor an enum.
|
|
565
|
+
|
|
566
|
+
Written as the Properties panel writes it: density, friction,
|
|
567
|
+
elasticity, and optionally the two weights. Handled here because it is a
|
|
568
|
+
real property on every BasePart and the fall-through below cannot take
|
|
569
|
+
it -- see the note on the Enum lookup.
|
|
570
|
+
]]
|
|
571
|
+
if valueType == "PhysicalProperties" then
|
|
572
|
+
if #numbers >= 5 then
|
|
573
|
+
return true, PhysicalProperties.new(numbers[1], numbers[2], numbers[3], numbers[4], numbers[5]), nil
|
|
574
|
+
end
|
|
575
|
+
if #numbers >= 3 then
|
|
576
|
+
return true, PhysicalProperties.new(numbers[1], numbers[2], numbers[3]), nil
|
|
577
|
+
end
|
|
578
|
+
return false, nil, 'expected "density, friction, elasticity" and optionally two weights'
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
--[[
|
|
582
|
+
Fonts, which look like an enum and are not one.
|
|
583
|
+
|
|
584
|
+
`TextLabel.FontFace` holds a `Font`, while `Enum.Font` is a separate
|
|
585
|
+
thing that happens to share its names. The fall-through below found
|
|
586
|
+
`Enum.Font` first and returned an EnumItem, so every attempt to set
|
|
587
|
+
FontFace failed with "Font expected, got EnumItem" -- measured while
|
|
588
|
+
building an ordinary shop panel, where it stopped the whole batch. This
|
|
589
|
+
has to come first for that reason.
|
|
590
|
+
|
|
591
|
+
Three spellings are accepted, because all three are what someone
|
|
592
|
+
actually has to hand: the familiar enum name ("GothamBold"), a family
|
|
593
|
+
asset id, and a family with a weight and style after it -- the form this
|
|
594
|
+
module writes when it reads a Font back.
|
|
595
|
+
]]
|
|
596
|
+
if valueType == "Font" then
|
|
597
|
+
local written = tostring(text)
|
|
598
|
+
local pieces: { string } = {}
|
|
599
|
+
for piece in string.gmatch(written, "[^|]+") do
|
|
600
|
+
table.insert(pieces, (string.gsub(piece, "^%s*(.-)%s*$", "%1")))
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
local family = if #pieces > 0 then pieces[1] else written
|
|
604
|
+
local weight = Enum.FontWeight.Regular
|
|
605
|
+
local style = Enum.FontStyle.Normal
|
|
606
|
+
if #pieces >= 2 then
|
|
607
|
+
local okWeight, found = pcall(function()
|
|
608
|
+
return (Enum.FontWeight :: any)[pieces[2]]
|
|
609
|
+
end)
|
|
610
|
+
if okWeight and found then
|
|
611
|
+
weight = found
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
if #pieces >= 3 and pieces[3] == "Italic" then
|
|
615
|
+
style = Enum.FontStyle.Italic
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
-- The enum spelling first: "GothamBold" carries its weight in the name,
|
|
619
|
+
-- and `fromEnum` knows the pairing better than any table here would.
|
|
620
|
+
if #pieces == 1 then
|
|
621
|
+
local name = family:match("([^%.]+)$") or family
|
|
622
|
+
for _, item in Enum.Font:GetEnumItems() do
|
|
623
|
+
if item.Name == name and item ~= Enum.Font.Unknown then
|
|
624
|
+
local okEnumFont, font = pcall(function()
|
|
625
|
+
return Font.fromEnum(item)
|
|
626
|
+
end)
|
|
627
|
+
if okEnumFont then
|
|
628
|
+
return true, font, nil
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
if string.sub(family, 1, 9) == "rbxasset:" or string.sub(family, 1, 12) == "rbxassetid:" then
|
|
635
|
+
return true, Font.new(family, weight, style), nil
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
local okName, font = pcall(function()
|
|
639
|
+
return Font.fromName(family, weight, style)
|
|
640
|
+
end)
|
|
641
|
+
if okName and font then
|
|
642
|
+
return true, font, nil
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
return false,
|
|
646
|
+
nil,
|
|
647
|
+
string.format(
|
|
648
|
+
'"%s" is not a font. Use an Enum.Font name like "GothamBold", a family '
|
|
649
|
+
.. 'name like "Gotham", or "family | Weight | Style".',
|
|
650
|
+
written
|
|
651
|
+
)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
--[[
|
|
655
|
+
Enum values arrive as "Enum.Material.Plastic" or bare "Plastic".
|
|
656
|
+
|
|
657
|
+
Indexed inside a pcall, because `Enum` throws on an unknown name rather
|
|
658
|
+
than returning nil. Every value type this function does not handle ends
|
|
659
|
+
up here, so an unguarded lookup turned "no conversion known for type X"
|
|
660
|
+
-- a clear message naming the property -- into a raw HANDLER_ERROR
|
|
661
|
+
reading "PhysicalProperties is not a valid member of Enum", which names
|
|
662
|
+
the wrong thing entirely. Measured on a `modify` setting
|
|
663
|
+
CustomPhysicalProperties.
|
|
664
|
+
]]
|
|
665
|
+
local okEnum, enumType = pcall(function()
|
|
666
|
+
return (Enum :: any)[valueType]
|
|
667
|
+
end)
|
|
668
|
+
if okEnum and enumType then
|
|
669
|
+
local name = tostring(text):match("([^%.]+)$") or tostring(text)
|
|
670
|
+
for _, item in enumType:GetEnumItems() do
|
|
671
|
+
if item.Name == name then
|
|
672
|
+
return true, item, nil
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
return false,
|
|
676
|
+
nil,
|
|
677
|
+
string.format('"%s" is not a member of Enum.%s', tostring(text), valueType)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
--[[
|
|
681
|
+
Instance references, written as the path of the thing pointed at.
|
|
682
|
+
|
|
683
|
+
Motor6D.Part0, WeldConstraint.Part1, ObjectValue.Value, Beam.Attachment0,
|
|
684
|
+
Model.PrimaryPart, BillboardGui.Adornee -- every one of these holds
|
|
685
|
+
another instance, and none of them could be set at all before this. That
|
|
686
|
+
is not a corner: a Motor6D with no Part0 joins nothing, so building a rig
|
|
687
|
+
or welding a gun to a hand fell out of `create` entirely and had to go
|
|
688
|
+
through `execute_luau`. Measured on a six-joint R6 rig, which failed with
|
|
689
|
+
"no conversion known for type BasePart".
|
|
690
|
+
|
|
691
|
+
Written the same way every reference is READ -- `Serialize.value` returns
|
|
692
|
+
`GetFullName()` for an instance -- so a value read here can be written
|
|
693
|
+
straight back, which is the promise the rest of this module makes.
|
|
694
|
+
|
|
695
|
+
The class is checked against the property's own type, because the engine
|
|
696
|
+
accepts the assignment and then quietly does nothing useful with a Part0
|
|
697
|
+
pointing at a Folder. An empty string clears the reference, which is the
|
|
698
|
+
only way to say "nothing" in a field that otherwise takes a path.
|
|
699
|
+
]]
|
|
700
|
+
if kind == "string" then
|
|
701
|
+
local wanted = tostring(text)
|
|
702
|
+
if wanted == "" then
|
|
703
|
+
return true, nil, nil
|
|
704
|
+
end
|
|
705
|
+
local okPath, resolved = pcall(Paths.resolve, wanted)
|
|
706
|
+
if okPath and typeof(resolved) == "Instance" then
|
|
707
|
+
local instance = resolved :: Instance
|
|
708
|
+
if instance:IsA(valueType) then
|
|
709
|
+
return true, instance, nil
|
|
710
|
+
end
|
|
711
|
+
return false,
|
|
712
|
+
nil,
|
|
713
|
+
string.format(
|
|
714
|
+
"%s is a %s, but this property holds a %s",
|
|
715
|
+
wanted,
|
|
716
|
+
instance.ClassName,
|
|
717
|
+
valueType
|
|
718
|
+
)
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
--[[
|
|
722
|
+
The path did not resolve, and saying so is the whole point.
|
|
723
|
+
|
|
724
|
+
This used to fall through to the "no conversion known for type X"
|
|
725
|
+
line below, which blames the TYPE for what is really a missing
|
|
726
|
+
instance: setting `HingeConstraint.Attachment0` to a path that did
|
|
727
|
+
not exist yet reported `no conversion known for type "Attachment"`,
|
|
728
|
+
sending the reader to look for a serializer that was never the
|
|
729
|
+
problem. Measured while building a hinge in one `create` call.
|
|
730
|
+
]]
|
|
731
|
+
--[[
|
|
732
|
+
Only text that actually looks like a path is reported as a missing
|
|
733
|
+
instance -- and only that text is worth retrying later.
|
|
734
|
+
|
|
735
|
+
Without this check every unparseable string took this branch: a
|
|
736
|
+
ColorSequence written as "0=1, 0, 0" came back as `nothing at
|
|
737
|
+
"0=1, 0, 0"`, which names the wrong problem entirely and sends the
|
|
738
|
+
create handler off to retry something no amount of waiting will fix.
|
|
739
|
+
|
|
740
|
+
The test is whether the first segment names something real at the
|
|
741
|
+
root of the data model. "Workspace.Nowhere" passes it -- Workspace
|
|
742
|
+
exists, the rest does not, which is exactly the retryable case --
|
|
743
|
+
while "0.5" and "0=1, 0, 0" do not.
|
|
744
|
+
]]
|
|
745
|
+
local head = string.match(wanted, "^([%a_][%w_]*)%.") or string.match(wanted, "^([%a_][%w_]*)$")
|
|
746
|
+
local looksLikePath = head ~= nil and pcall(function()
|
|
747
|
+
return game:FindFirstChild(head :: string) ~= nil or game:GetService(head :: any) ~= nil
|
|
748
|
+
end)
|
|
749
|
+
if looksLikePath then
|
|
750
|
+
return false,
|
|
751
|
+
nil,
|
|
752
|
+
string.format('%snothing at "%s" (this property holds a %s)', UNRESOLVED_PREFIX, wanted, valueType)
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
return false, nil, string.format('no conversion known for type "%s"', valueType)
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
return Serialize
|