@el4cteo/rbx-studio-mcp 0.6.1 → 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 +8 -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/anim.js +159 -0
- package/dist/tools/anim.js.map +1 -0
- package/dist/tools/audio.js +96 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/character.js +95 -5
- package/dist/tools/character.js.map +1 -1
- package/dist/tools/data.js +292 -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 +96 -2
- 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 +162 -6
- 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 +675 -51
- package/dist/tools/world.js.map +1 -1
- package/package.json +2 -2
- package/plugin/src/Commands.luau +31 -7
- 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 +816 -618
- package/plugin/src/Png.luau +8 -4
- package/plugin/src/Prompt.luau +965 -961
- package/plugin/src/Secret.luau +86 -0
- package/plugin/src/Serialize.luau +440 -8
- package/plugin/src/Undo.luau +94 -6
- package/plugin/src/handlers/Anim.luau +897 -0
- package/plugin/src/handlers/Assets.luau +286 -2
- package/plugin/src/handlers/Audio.luau +411 -0
- 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 +722 -450
- package/plugin/src/handlers/Instances.luau +84 -4
- 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/Spatial.luau +334 -0
- package/plugin/src/handlers/Viewport.luau +268 -0
- package/plugin/src/handlers/World.luau +89 -15
- package/plugin/src/init.server.luau +9 -1
- package/scripts/build-plugin.mjs +20 -0
- package/scripts/check-plugin.mjs +171 -124
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Keeps a typed secret out of everything that remembers.
|
|
4
|
+
|
|
5
|
+
The panel is built to remember what you type. The line is echoed into the log
|
|
6
|
+
under "you", it goes into the up-arrow history, the log can be copied to the
|
|
7
|
+
clipboard with `copy`, and the whole widget ends up in screenshots -- which
|
|
8
|
+
this very server takes on request. Every one of those is correct behaviour
|
|
9
|
+
for `theme dark` and a disaster for an API key: a credential that can publish
|
|
10
|
+
assets under the user's name would sit in a scrollback that gets pasted into
|
|
11
|
+
a bug report.
|
|
12
|
+
|
|
13
|
+
So the one command that carries a secret is masked at every point where a
|
|
14
|
+
line is kept, and that masking lives here rather than in `Commands` because
|
|
15
|
+
two unrelated files -- the echo and the history -- both have to do it, and a
|
|
16
|
+
secret protected in one of two places is not protected.
|
|
17
|
+
|
|
18
|
+
Masking, not dropping. `cloud key ****` in the log still tells the user that
|
|
19
|
+
the command ran; a line that vanished would read as the panel ignoring them,
|
|
20
|
+
and the next thing a person does about that is type it again.
|
|
21
|
+
]]
|
|
22
|
+
|
|
23
|
+
local Secret = {}
|
|
24
|
+
|
|
25
|
+
--[[
|
|
26
|
+
Commands whose arguments are secret, and from which word onwards.
|
|
27
|
+
|
|
28
|
+
Keyed by "command subcommand" so `cloud key <secret>` is covered and `cloud
|
|
29
|
+
test` -- which carries nothing -- is not masked into uselessness. The user id
|
|
30
|
+
is masked too. It is not a credential and it is on a public profile URL, but
|
|
31
|
+
it names the account the key belongs to, and a screenshot showing both is
|
|
32
|
+
worth more to somebody than a screenshot showing either.
|
|
33
|
+
]]
|
|
34
|
+
--[[
|
|
35
|
+
Commands whose arguments are secret, and from which word onwards.
|
|
36
|
+
|
|
37
|
+
Keyed by "command subcommand" so `cloud key <secret>` is covered and `cloud
|
|
38
|
+
test` -- which carries nothing -- is not masked into uselessness.
|
|
39
|
+
|
|
40
|
+
ONLY the key. The user, group, universe and place ids were masked here too
|
|
41
|
+
at first, which was theatre: they are public -- a user id is in the profile
|
|
42
|
+
URL and a universe id is on the game's dashboard page -- and the reply that
|
|
43
|
+
confirms each one prints it back in full anyway, so the dots hid a value
|
|
44
|
+
from the same log line that then showed it. Masking something that is about
|
|
45
|
+
to be displayed teaches a reader that the dots mean nothing, which is
|
|
46
|
+
exactly the wrong lesson for the one line where they do.
|
|
47
|
+
]]
|
|
48
|
+
local SECRET_FROM: { [string]: number } = {
|
|
49
|
+
["cloud key"] = 3,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
--[[
|
|
53
|
+
Returns the line as it is safe to display, and whether anything was hidden.
|
|
54
|
+
|
|
55
|
+
The caller gets the flag because "this was masked" is worth saying once, next
|
|
56
|
+
to the echoed line -- otherwise a user who sees dots where they typed a key
|
|
57
|
+
cannot tell whether the panel hid it or mangled it.
|
|
58
|
+
]]
|
|
59
|
+
function Secret.mask(line: string): (string, boolean)
|
|
60
|
+
local words: { string } = {}
|
|
61
|
+
for word in string.gmatch(line, "%S+") do
|
|
62
|
+
table.insert(words, word)
|
|
63
|
+
end
|
|
64
|
+
if #words < 2 then
|
|
65
|
+
return line, false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
local from = SECRET_FROM[string.lower(words[1]) .. " " .. string.lower(words[2])]
|
|
69
|
+
if from == nil or #words < from then
|
|
70
|
+
return line, false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
--[[
|
|
74
|
+
One fixed marker for everything from `from` on, not one per character
|
|
75
|
+
and not one per word.
|
|
76
|
+
|
|
77
|
+
Length is information: eight dots against thirty says which of two things
|
|
78
|
+
was typed, and for a key it narrows a guess. Every masked value looks
|
|
79
|
+
identical, whatever was actually there.
|
|
80
|
+
]]
|
|
81
|
+
local masked = table.move(words, 1, from - 1, 1, {} :: { string })
|
|
82
|
+
table.insert(masked, "••••••••")
|
|
83
|
+
return table.concat(masked, " "), true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
return Secret
|
|
@@ -13,8 +13,21 @@
|
|
|
13
13
|
straight back without the agent reformatting it.
|
|
14
14
|
]]
|
|
15
15
|
|
|
16
|
+
local Paths = require(script.Parent.Paths)
|
|
17
|
+
|
|
16
18
|
local Serialize = {}
|
|
17
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
|
+
|
|
18
31
|
-- Floats are rounded before display: Studio reports positions like
|
|
19
32
|
-- 12.000000476837158, which costs tokens and means nothing to the caller.
|
|
20
33
|
local PRECISION = 4
|
|
@@ -87,11 +100,54 @@ function Serialize.value(value: any): any
|
|
|
87
100
|
if kind == "UDim" then
|
|
88
101
|
return string.format("{%s, %s}", num(value.Scale), num(value.Offset))
|
|
89
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
|
|
90
119
|
if kind == "EnumItem" then
|
|
91
120
|
return string.format("Enum.%s.%s", tostring(value.EnumType), value.Name)
|
|
92
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
|
|
93
140
|
if kind == "Instance" then
|
|
94
|
-
|
|
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)
|
|
95
151
|
end
|
|
96
152
|
if kind == "NumberRange" then
|
|
97
153
|
return string.format("%s..%s", num(value.Min), num(value.Max))
|
|
@@ -176,6 +232,51 @@ local function scanTokens(text: string): { number }
|
|
|
176
232
|
return numbers
|
|
177
233
|
end
|
|
178
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
|
+
|
|
179
280
|
--[[
|
|
180
281
|
Parses a serialized string back into a Roblox value, given the target type
|
|
181
282
|
from the API dump. Returns ok=false with a reason the agent can act on
|
|
@@ -194,9 +295,48 @@ function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
|
194
295
|
return true, text, nil
|
|
195
296
|
end
|
|
196
297
|
|
|
197
|
-
if valueType == "string" or valueType == "
|
|
298
|
+
if valueType == "string" or valueType == "ProtectedString" then
|
|
198
299
|
return true, tostring(text), nil
|
|
199
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
|
|
200
340
|
if valueType == "bool" then
|
|
201
341
|
if kind == "boolean" then
|
|
202
342
|
return true, text, nil
|
|
@@ -300,16 +440,232 @@ function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
|
300
440
|
end
|
|
301
441
|
return false, nil, 'expected "pos x, y, z" or "pos x, y, z | rot rx, ry, rz"'
|
|
302
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
|
+
|
|
303
547
|
if valueType == "NumberRange" then
|
|
304
|
-
if
|
|
305
|
-
|
|
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
|
|
306
559
|
end
|
|
307
|
-
return
|
|
560
|
+
return false, nil, 'expected "min..max", e.g. "1..3"'
|
|
308
561
|
end
|
|
309
562
|
|
|
310
|
-
--
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
313
669
|
local name = tostring(text):match("([^%.]+)$") or tostring(text)
|
|
314
670
|
for _, item in enumType:GetEnumItems() do
|
|
315
671
|
if item.Name == name then
|
|
@@ -321,6 +677,82 @@ function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
|
321
677
|
string.format('"%s" is not a member of Enum.%s', tostring(text), valueType)
|
|
322
678
|
end
|
|
323
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
|
+
|
|
324
756
|
return false, nil, string.format('no conversion known for type "%s"', valueType)
|
|
325
757
|
end
|
|
326
758
|
|