@el4cteo/rbx-studio-mcp 0.6.1 → 0.6.5
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 +4 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/anim.js +159 -0
- package/dist/tools/anim.js.map +1 -0
- package/dist/tools/character.js +95 -5
- package/dist/tools/character.js.map +1 -1
- package/dist/tools/data.js +173 -0
- package/dist/tools/data.js.map +1 -0
- package/dist/tools/device.js +77 -7
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/discover.js +80 -4
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +48 -1
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/input.js +35 -9
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/perf.js +74 -7
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +51 -3
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/world.js +317 -44
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Commands.luau +622 -622
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Console.luau +1909 -1843
- package/plugin/src/Emulation.luau +172 -0
- package/plugin/src/Phrase.luau +164 -16
- package/plugin/src/Png.luau +8 -4
- package/plugin/src/Serialize.luau +499 -327
- package/plugin/src/Undo.luau +94 -6
- package/plugin/src/handlers/Anim.luau +897 -0
- package/plugin/src/handlers/Assets.luau +587 -352
- package/plugin/src/handlers/Capture.luau +155 -20
- package/plugin/src/handlers/Character.luau +823 -361
- package/plugin/src/handlers/Data.luau +539 -0
- package/plugin/src/handlers/Device.luau +394 -139
- package/plugin/src/handlers/Discover.luau +685 -363
- package/plugin/src/handlers/Geometry.luau +127 -0
- package/plugin/src/handlers/Perf.luau +227 -0
- package/plugin/src/handlers/Scripts.luau +673 -539
- package/plugin/src/handlers/Session.luau +3 -0
- package/plugin/src/handlers/Viewport.luau +268 -0
- package/plugin/src/handlers/World.luau +89 -15
- package/plugin/src/init.server.luau +879 -875
- package/scripts/build-plugin.mjs +20 -0
- package/scripts/check-plugin.mjs +171 -124
|
@@ -1,327 +1,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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
num(position.
|
|
67
|
-
num(
|
|
68
|
-
num(
|
|
69
|
-
num(math.deg(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
num(value.
|
|
84
|
-
num(value.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
107
|
-
end
|
|
108
|
-
if kind == "
|
|
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
|
-
end
|
|
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
|
-
end
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if
|
|
251
|
-
return false, nil, 'expected
|
|
252
|
-
end
|
|
253
|
-
return true,
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
+
-- Floats are rounded before display: Studio reports positions like
|
|
21
|
+
-- 12.000000476837158, which costs tokens and means nothing to the caller.
|
|
22
|
+
local PRECISION = 4
|
|
23
|
+
|
|
24
|
+
-- API-dump type names that accept a plain Luau number.
|
|
25
|
+
local NUMERIC_TYPES: { [string]: boolean } = {
|
|
26
|
+
int = true,
|
|
27
|
+
int64 = true,
|
|
28
|
+
float = true,
|
|
29
|
+
double = true,
|
|
30
|
+
number = true,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
local function num(value: number): string
|
|
34
|
+
local rounded = math.round(value * 10 ^ PRECISION) / 10 ^ PRECISION
|
|
35
|
+
if rounded == math.floor(rounded) and math.abs(rounded) < 1e15 then
|
|
36
|
+
return string.format("%d", rounded)
|
|
37
|
+
end
|
|
38
|
+
return tostring(rounded)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
--[[
|
|
42
|
+
Serializes one value. Unknown userdata falls back to `tostring`, which is
|
|
43
|
+
always better than dropping the property silently.
|
|
44
|
+
]]
|
|
45
|
+
function Serialize.value(value: any): any
|
|
46
|
+
local kind = typeof(value)
|
|
47
|
+
|
|
48
|
+
if kind == "string" or kind == "boolean" or kind == "nil" then
|
|
49
|
+
return value
|
|
50
|
+
end
|
|
51
|
+
if kind == "number" then
|
|
52
|
+
-- Kept numeric so the agent can do arithmetic without parsing.
|
|
53
|
+
return math.round(value * 10 ^ PRECISION) / 10 ^ PRECISION
|
|
54
|
+
end
|
|
55
|
+
if kind == "Vector3" then
|
|
56
|
+
return string.format("%s, %s, %s", num(value.X), num(value.Y), num(value.Z))
|
|
57
|
+
end
|
|
58
|
+
if kind == "Vector2" then
|
|
59
|
+
return string.format("%s, %s", num(value.X), num(value.Y))
|
|
60
|
+
end
|
|
61
|
+
if kind == "CFrame" then
|
|
62
|
+
local position = value.Position
|
|
63
|
+
local rx, ry, rz = value:ToOrientation()
|
|
64
|
+
return string.format(
|
|
65
|
+
"pos %s, %s, %s | rot %s, %s, %s",
|
|
66
|
+
num(position.X),
|
|
67
|
+
num(position.Y),
|
|
68
|
+
num(position.Z),
|
|
69
|
+
num(math.deg(rx)),
|
|
70
|
+
num(math.deg(ry)),
|
|
71
|
+
num(math.deg(rz))
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
if kind == "Color3" then
|
|
75
|
+
return string.format("%s, %s, %s", num(value.R), num(value.G), num(value.B))
|
|
76
|
+
end
|
|
77
|
+
if kind == "BrickColor" then
|
|
78
|
+
return value.Name
|
|
79
|
+
end
|
|
80
|
+
if kind == "UDim2" then
|
|
81
|
+
return string.format(
|
|
82
|
+
"{%s, %s}, {%s, %s}",
|
|
83
|
+
num(value.X.Scale),
|
|
84
|
+
num(value.X.Offset),
|
|
85
|
+
num(value.Y.Scale),
|
|
86
|
+
num(value.Y.Offset)
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
if kind == "UDim" then
|
|
90
|
+
return string.format("{%s, %s}", num(value.Scale), num(value.Offset))
|
|
91
|
+
end
|
|
92
|
+
if kind == "Font" then
|
|
93
|
+
--[[
|
|
94
|
+
Written so it can be read back, which `tostring` is not.
|
|
95
|
+
|
|
96
|
+
A Font stringifies as `Font { Family = rbxasset://..., Weight = Bold,
|
|
97
|
+
Style = Normal }`, which nothing here can parse. The family, weight
|
|
98
|
+
and style are all there is to one, so they are written plainly and
|
|
99
|
+
separated the way a composite value is elsewhere in this module.
|
|
100
|
+
]]
|
|
101
|
+
return string.format(
|
|
102
|
+
"%s | %s | %s",
|
|
103
|
+
value.Family,
|
|
104
|
+
value.Weight.Name,
|
|
105
|
+
value.Style.Name
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
if kind == "EnumItem" then
|
|
109
|
+
return string.format("Enum.%s.%s", tostring(value.EnumType), value.Name)
|
|
110
|
+
end
|
|
111
|
+
if kind == "Instance" then
|
|
112
|
+
--[[
|
|
113
|
+
Formatted as an address, not as a display name.
|
|
114
|
+
|
|
115
|
+
`GetFullName` writes "StarterPack.Pistol.Handle" for both of two
|
|
116
|
+
Tools named Pistol, so reading a Motor6D's Part1 and writing it back
|
|
117
|
+
pointed at the wrong one -- silently, since both paths resolve.
|
|
118
|
+
`Paths.of` adds the [n] that tells them apart, which is what makes a
|
|
119
|
+
value read here safe to write straight back.
|
|
120
|
+
]]
|
|
121
|
+
return Paths.of(value)
|
|
122
|
+
end
|
|
123
|
+
if kind == "NumberRange" then
|
|
124
|
+
return string.format("%s..%s", num(value.Min), num(value.Max))
|
|
125
|
+
end
|
|
126
|
+
if kind == "Rect" then
|
|
127
|
+
return string.format(
|
|
128
|
+
"%s, %s, %s, %s",
|
|
129
|
+
num(value.Min.X),
|
|
130
|
+
num(value.Min.Y),
|
|
131
|
+
num(value.Max.X),
|
|
132
|
+
num(value.Max.Y)
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
if kind == "ColorSequence" then
|
|
136
|
+
local parts: { string } = {}
|
|
137
|
+
for _, keypoint in value.Keypoints do
|
|
138
|
+
table.insert(
|
|
139
|
+
parts,
|
|
140
|
+
string.format("%s=%s", num(keypoint.Time), Serialize.value(keypoint.Value))
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
return table.concat(parts, " ")
|
|
144
|
+
end
|
|
145
|
+
if kind == "NumberSequence" then
|
|
146
|
+
local parts: { string } = {}
|
|
147
|
+
for _, keypoint in value.Keypoints do
|
|
148
|
+
table.insert(parts, string.format("%s=%s", num(keypoint.Time), num(keypoint.Value)))
|
|
149
|
+
end
|
|
150
|
+
return table.concat(parts, " ")
|
|
151
|
+
end
|
|
152
|
+
if kind == "table" then
|
|
153
|
+
local copy: { [any]: any } = {}
|
|
154
|
+
for key, item in value :: { [any]: any } do
|
|
155
|
+
copy[key] = Serialize.value(item)
|
|
156
|
+
end
|
|
157
|
+
return copy
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
return tostring(value)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
--[[
|
|
164
|
+
Reads a property without throwing.
|
|
165
|
+
|
|
166
|
+
Property access on an instance can error even when the API dump says the
|
|
167
|
+
property exists -- some are context-gated (only readable during a playtest,
|
|
168
|
+
or only on a loaded asset). Returning nil for those keeps one awkward
|
|
169
|
+
property from failing an inspect of 50 instances.
|
|
170
|
+
]]
|
|
171
|
+
function Serialize.readProperty(instance: Instance, name: string): (boolean, any)
|
|
172
|
+
local ok, value = pcall(function()
|
|
173
|
+
return (instance :: any)[name]
|
|
174
|
+
end)
|
|
175
|
+
if not ok then
|
|
176
|
+
return false, nil
|
|
177
|
+
end
|
|
178
|
+
return true, Serialize.value(value)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
--[[
|
|
182
|
+
Pulls the numbers out of a composite value written as text.
|
|
183
|
+
|
|
184
|
+
This used to be a single `-?%d+%.?%d*` scan, which is not the grammar Luau
|
|
185
|
+
numbers actually have: "1e3, 0, .5" came back as 1, 3 and 0, so a Position
|
|
186
|
+
written in scientific notation was silently set to a different point in the
|
|
187
|
+
world and reported as applied. Splitting on the separators and handing each
|
|
188
|
+
token to `tonumber` means this agrees with the scalar path, which always
|
|
189
|
+
used `tonumber` -- the two disagreeing about what counts as a number was the
|
|
190
|
+
whole bug.
|
|
191
|
+
|
|
192
|
+
Non-numeric tokens are dropped, so "Vector3.new(1, 2, 3)" and "{1, 2, 3}"
|
|
193
|
+
still read as three numbers.
|
|
194
|
+
]]
|
|
195
|
+
local function scanTokens(text: string): { number }
|
|
196
|
+
local numbers: { number } = {}
|
|
197
|
+
for token in string.gmatch(text, "[^,%s%(%)%[%]{}<>]+") do
|
|
198
|
+
local value = tonumber(token)
|
|
199
|
+
if value then
|
|
200
|
+
table.insert(numbers, value)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
return numbers
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
--[[
|
|
207
|
+
Parses a serialized string back into a Roblox value, given the target type
|
|
208
|
+
from the API dump. Returns ok=false with a reason the agent can act on
|
|
209
|
+
rather than raising, so batch writes can report per-property failures.
|
|
210
|
+
]]
|
|
211
|
+
function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
212
|
+
local kind = typeof(text)
|
|
213
|
+
|
|
214
|
+
-- Values already of the right primitive shape pass straight through. The
|
|
215
|
+
-- number case is matched against the target type rather than excluded from
|
|
216
|
+
-- one: a bare 5 is a valid `float`, but not a valid Vector3, Enum or Color3.
|
|
217
|
+
if kind == "boolean" then
|
|
218
|
+
return true, text, nil
|
|
219
|
+
end
|
|
220
|
+
if kind == "number" and NUMERIC_TYPES[valueType] then
|
|
221
|
+
return true, text, nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if valueType == "string" or valueType == "Content" or valueType == "ProtectedString" then
|
|
225
|
+
return true, tostring(text), nil
|
|
226
|
+
end
|
|
227
|
+
if valueType == "bool" then
|
|
228
|
+
if kind == "boolean" then
|
|
229
|
+
return true, text, nil
|
|
230
|
+
end
|
|
231
|
+
if kind == "number" then
|
|
232
|
+
return true, text ~= 0, nil
|
|
233
|
+
end
|
|
234
|
+
-- This used to be `text == "true"`, which quietly turned "True", "TRUE"
|
|
235
|
+
-- and "1" into false and reported the write as applied. A bool that is
|
|
236
|
+
-- silently the opposite of what was asked is the worst failure this
|
|
237
|
+
-- module can produce, so near misses are accepted and anything else is
|
|
238
|
+
-- refused rather than guessed at.
|
|
239
|
+
local lowered = string.lower(tostring(text))
|
|
240
|
+
if lowered == "true" or lowered == "1" or lowered == "yes" then
|
|
241
|
+
return true, true, nil
|
|
242
|
+
end
|
|
243
|
+
if lowered == "false" or lowered == "0" or lowered == "no" then
|
|
244
|
+
return true, false, nil
|
|
245
|
+
end
|
|
246
|
+
return false, nil, string.format('expected true or false, got "%s"', tostring(text))
|
|
247
|
+
end
|
|
248
|
+
if NUMERIC_TYPES[valueType] then
|
|
249
|
+
local parsed = tonumber(text)
|
|
250
|
+
if not parsed then
|
|
251
|
+
return false, nil, string.format('expected a number, got "%s"', tostring(text))
|
|
252
|
+
end
|
|
253
|
+
return true, parsed, nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
local numbers: { number } = {}
|
|
257
|
+
if kind == "string" then
|
|
258
|
+
for _, token in scanTokens(text) do
|
|
259
|
+
table.insert(numbers, token)
|
|
260
|
+
end
|
|
261
|
+
elseif kind == "table" then
|
|
262
|
+
for _, item in text :: { any } do
|
|
263
|
+
local parsed = tonumber(item)
|
|
264
|
+
if parsed then
|
|
265
|
+
table.insert(numbers, parsed)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
if valueType == "Vector3" then
|
|
271
|
+
if #numbers < 3 then
|
|
272
|
+
return false, nil, 'expected three numbers, e.g. "12, 0, 5"'
|
|
273
|
+
end
|
|
274
|
+
return true, Vector3.new(numbers[1], numbers[2], numbers[3]), nil
|
|
275
|
+
end
|
|
276
|
+
if valueType == "Vector2" then
|
|
277
|
+
if #numbers < 2 then
|
|
278
|
+
return false, nil, 'expected two numbers, e.g. "12, 5"'
|
|
279
|
+
end
|
|
280
|
+
return true, Vector2.new(numbers[1], numbers[2]), nil
|
|
281
|
+
end
|
|
282
|
+
if valueType == "Color3" then
|
|
283
|
+
if #numbers < 3 then
|
|
284
|
+
return false, nil, 'expected three 0-1 components, e.g. "1, 0.5, 0"'
|
|
285
|
+
end
|
|
286
|
+
-- 0-255 is a common mistake and unambiguous to detect, so accept it.
|
|
287
|
+
local scale = if numbers[1] > 1 or numbers[2] > 1 or numbers[3] > 1 then 255 else 1
|
|
288
|
+
return true, Color3.new(numbers[1] / scale, numbers[2] / scale, numbers[3] / scale), nil
|
|
289
|
+
end
|
|
290
|
+
if valueType == "BrickColor" then
|
|
291
|
+
-- Cast because the API definitions type this overload as a literal union
|
|
292
|
+
-- of every BrickColor name. At runtime it takes any string and throws on
|
|
293
|
+
-- an unknown one, which is exactly what the pcall is here to catch.
|
|
294
|
+
local ok, color = pcall(function()
|
|
295
|
+
return BrickColor.new(tostring(text) :: any)
|
|
296
|
+
end)
|
|
297
|
+
if not ok then
|
|
298
|
+
return false, nil, string.format('"%s" is not a BrickColor name', tostring(text))
|
|
299
|
+
end
|
|
300
|
+
return true, color, nil
|
|
301
|
+
end
|
|
302
|
+
if valueType == "UDim2" then
|
|
303
|
+
if #numbers < 4 then
|
|
304
|
+
return false, nil, 'expected four numbers, e.g. "{0.5, 10}, {0.5, 20}"'
|
|
305
|
+
end
|
|
306
|
+
return true, UDim2.new(numbers[1], numbers[2], numbers[3], numbers[4]), nil
|
|
307
|
+
end
|
|
308
|
+
if valueType == "UDim" then
|
|
309
|
+
if #numbers < 2 then
|
|
310
|
+
return false, nil, 'expected two numbers, e.g. "{0.5, 10}"'
|
|
311
|
+
end
|
|
312
|
+
return true, UDim.new(numbers[1], numbers[2]), nil
|
|
313
|
+
end
|
|
314
|
+
if valueType == "CFrame" then
|
|
315
|
+
if #numbers >= 6 then
|
|
316
|
+
return true,
|
|
317
|
+
CFrame.new(numbers[1], numbers[2], numbers[3])
|
|
318
|
+
* CFrame.fromOrientation(
|
|
319
|
+
math.rad(numbers[4]),
|
|
320
|
+
math.rad(numbers[5]),
|
|
321
|
+
math.rad(numbers[6])
|
|
322
|
+
),
|
|
323
|
+
nil
|
|
324
|
+
end
|
|
325
|
+
if #numbers >= 3 then
|
|
326
|
+
return true, CFrame.new(numbers[1], numbers[2], numbers[3]), nil
|
|
327
|
+
end
|
|
328
|
+
return false, nil, 'expected "pos x, y, z" or "pos x, y, z | rot rx, ry, rz"'
|
|
329
|
+
end
|
|
330
|
+
if valueType == "NumberRange" then
|
|
331
|
+
if #numbers < 2 then
|
|
332
|
+
return false, nil, 'expected "min..max"'
|
|
333
|
+
end
|
|
334
|
+
return true, NumberRange.new(numbers[1], numbers[2]), nil
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
--[[
|
|
338
|
+
PhysicalProperties, which is neither a vector nor an enum.
|
|
339
|
+
|
|
340
|
+
Written as the Properties panel writes it: density, friction,
|
|
341
|
+
elasticity, and optionally the two weights. Handled here because it is a
|
|
342
|
+
real property on every BasePart and the fall-through below cannot take
|
|
343
|
+
it -- see the note on the Enum lookup.
|
|
344
|
+
]]
|
|
345
|
+
if valueType == "PhysicalProperties" then
|
|
346
|
+
if #numbers >= 5 then
|
|
347
|
+
return true, PhysicalProperties.new(numbers[1], numbers[2], numbers[3], numbers[4], numbers[5]), nil
|
|
348
|
+
end
|
|
349
|
+
if #numbers >= 3 then
|
|
350
|
+
return true, PhysicalProperties.new(numbers[1], numbers[2], numbers[3]), nil
|
|
351
|
+
end
|
|
352
|
+
return false, nil, 'expected "density, friction, elasticity" and optionally two weights'
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
--[[
|
|
356
|
+
Fonts, which look like an enum and are not one.
|
|
357
|
+
|
|
358
|
+
`TextLabel.FontFace` holds a `Font`, while `Enum.Font` is a separate
|
|
359
|
+
thing that happens to share its names. The fall-through below found
|
|
360
|
+
`Enum.Font` first and returned an EnumItem, so every attempt to set
|
|
361
|
+
FontFace failed with "Font expected, got EnumItem" -- measured while
|
|
362
|
+
building an ordinary shop panel, where it stopped the whole batch. This
|
|
363
|
+
has to come first for that reason.
|
|
364
|
+
|
|
365
|
+
Three spellings are accepted, because all three are what someone
|
|
366
|
+
actually has to hand: the familiar enum name ("GothamBold"), a family
|
|
367
|
+
asset id, and a family with a weight and style after it -- the form this
|
|
368
|
+
module writes when it reads a Font back.
|
|
369
|
+
]]
|
|
370
|
+
if valueType == "Font" then
|
|
371
|
+
local written = tostring(text)
|
|
372
|
+
local pieces: { string } = {}
|
|
373
|
+
for piece in string.gmatch(written, "[^|]+") do
|
|
374
|
+
table.insert(pieces, (string.gsub(piece, "^%s*(.-)%s*$", "%1")))
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
local family = if #pieces > 0 then pieces[1] else written
|
|
378
|
+
local weight = Enum.FontWeight.Regular
|
|
379
|
+
local style = Enum.FontStyle.Normal
|
|
380
|
+
if #pieces >= 2 then
|
|
381
|
+
local okWeight, found = pcall(function()
|
|
382
|
+
return (Enum.FontWeight :: any)[pieces[2]]
|
|
383
|
+
end)
|
|
384
|
+
if okWeight and found then
|
|
385
|
+
weight = found
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
if #pieces >= 3 and pieces[3] == "Italic" then
|
|
389
|
+
style = Enum.FontStyle.Italic
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
-- The enum spelling first: "GothamBold" carries its weight in the name,
|
|
393
|
+
-- and `fromEnum` knows the pairing better than any table here would.
|
|
394
|
+
if #pieces == 1 then
|
|
395
|
+
local name = family:match("([^%.]+)$") or family
|
|
396
|
+
for _, item in Enum.Font:GetEnumItems() do
|
|
397
|
+
if item.Name == name and item ~= Enum.Font.Unknown then
|
|
398
|
+
local okEnumFont, font = pcall(function()
|
|
399
|
+
return Font.fromEnum(item)
|
|
400
|
+
end)
|
|
401
|
+
if okEnumFont then
|
|
402
|
+
return true, font, nil
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
if string.sub(family, 1, 9) == "rbxasset:" or string.sub(family, 1, 12) == "rbxassetid:" then
|
|
409
|
+
return true, Font.new(family, weight, style), nil
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
local okName, font = pcall(function()
|
|
413
|
+
return Font.fromName(family, weight, style)
|
|
414
|
+
end)
|
|
415
|
+
if okName and font then
|
|
416
|
+
return true, font, nil
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
return false,
|
|
420
|
+
nil,
|
|
421
|
+
string.format(
|
|
422
|
+
'"%s" is not a font. Use an Enum.Font name like "GothamBold", a family '
|
|
423
|
+
.. 'name like "Gotham", or "family | Weight | Style".',
|
|
424
|
+
written
|
|
425
|
+
)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
--[[
|
|
429
|
+
Enum values arrive as "Enum.Material.Plastic" or bare "Plastic".
|
|
430
|
+
|
|
431
|
+
Indexed inside a pcall, because `Enum` throws on an unknown name rather
|
|
432
|
+
than returning nil. Every value type this function does not handle ends
|
|
433
|
+
up here, so an unguarded lookup turned "no conversion known for type X"
|
|
434
|
+
-- a clear message naming the property -- into a raw HANDLER_ERROR
|
|
435
|
+
reading "PhysicalProperties is not a valid member of Enum", which names
|
|
436
|
+
the wrong thing entirely. Measured on a `modify` setting
|
|
437
|
+
CustomPhysicalProperties.
|
|
438
|
+
]]
|
|
439
|
+
local okEnum, enumType = pcall(function()
|
|
440
|
+
return (Enum :: any)[valueType]
|
|
441
|
+
end)
|
|
442
|
+
if okEnum and enumType then
|
|
443
|
+
local name = tostring(text):match("([^%.]+)$") or tostring(text)
|
|
444
|
+
for _, item in enumType:GetEnumItems() do
|
|
445
|
+
if item.Name == name then
|
|
446
|
+
return true, item, nil
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
return false,
|
|
450
|
+
nil,
|
|
451
|
+
string.format('"%s" is not a member of Enum.%s', tostring(text), valueType)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
--[[
|
|
455
|
+
Instance references, written as the path of the thing pointed at.
|
|
456
|
+
|
|
457
|
+
Motor6D.Part0, WeldConstraint.Part1, ObjectValue.Value, Beam.Attachment0,
|
|
458
|
+
Model.PrimaryPart, BillboardGui.Adornee -- every one of these holds
|
|
459
|
+
another instance, and none of them could be set at all before this. That
|
|
460
|
+
is not a corner: a Motor6D with no Part0 joins nothing, so building a rig
|
|
461
|
+
or welding a gun to a hand fell out of `create` entirely and had to go
|
|
462
|
+
through `execute_luau`. Measured on a six-joint R6 rig, which failed with
|
|
463
|
+
"no conversion known for type BasePart".
|
|
464
|
+
|
|
465
|
+
Written the same way every reference is READ -- `Serialize.value` returns
|
|
466
|
+
`GetFullName()` for an instance -- so a value read here can be written
|
|
467
|
+
straight back, which is the promise the rest of this module makes.
|
|
468
|
+
|
|
469
|
+
The class is checked against the property's own type, because the engine
|
|
470
|
+
accepts the assignment and then quietly does nothing useful with a Part0
|
|
471
|
+
pointing at a Folder. An empty string clears the reference, which is the
|
|
472
|
+
only way to say "nothing" in a field that otherwise takes a path.
|
|
473
|
+
]]
|
|
474
|
+
if kind == "string" then
|
|
475
|
+
local wanted = tostring(text)
|
|
476
|
+
if wanted == "" then
|
|
477
|
+
return true, nil, nil
|
|
478
|
+
end
|
|
479
|
+
local okPath, resolved = pcall(Paths.resolve, wanted)
|
|
480
|
+
if okPath and typeof(resolved) == "Instance" then
|
|
481
|
+
local instance = resolved :: Instance
|
|
482
|
+
if instance:IsA(valueType) then
|
|
483
|
+
return true, instance, nil
|
|
484
|
+
end
|
|
485
|
+
return false,
|
|
486
|
+
nil,
|
|
487
|
+
string.format(
|
|
488
|
+
"%s is a %s, but this property holds a %s",
|
|
489
|
+
wanted,
|
|
490
|
+
instance.ClassName,
|
|
491
|
+
valueType
|
|
492
|
+
)
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
return false, nil, string.format('no conversion known for type "%s"', valueType)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
return Serialize
|