@el4cteo/rbx-studio-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/bridge/rpc.js +243 -0
- package/dist/bridge/rpc.js.map +1 -0
- package/dist/bridge/server.js +281 -0
- package/dist/bridge/server.js.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/apidump.js +269 -0
- package/dist/lib/apidump.js.map +1 -0
- package/dist/lib/errors.js +38 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/format.js +191 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/pluginbuild.js +83 -0
- package/dist/lib/pluginbuild.js.map +1 -0
- package/dist/lib/png.js +84 -0
- package/dist/lib/png.js.map +1 -0
- package/dist/lib/protocol.js +22 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/tool.js +27 -0
- package/dist/lib/tool.js.map +1 -0
- package/dist/resources.js +70 -0
- package/dist/resources.js.map +1 -0
- package/dist/tools/api.js +78 -0
- package/dist/tools/api.js.map +1 -0
- package/dist/tools/character.js +94 -0
- package/dist/tools/character.js.map +1 -0
- package/dist/tools/debug.js +211 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/device.js +74 -0
- package/dist/tools/device.js.map +1 -0
- package/dist/tools/discover.js +217 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/exec.js +191 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/input.js +96 -0
- package/dist/tools/input.js.map +1 -0
- package/dist/tools/instances.js +261 -0
- package/dist/tools/instances.js.map +1 -0
- package/dist/tools/perf.js +367 -0
- package/dist/tools/perf.js.map +1 -0
- package/dist/tools/playtest.js +153 -0
- package/dist/tools/playtest.js.map +1 -0
- package/dist/tools/screenshot.js +75 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/scripts.js +316 -0
- package/dist/tools/scripts.js.map +1 -0
- package/dist/tools/session.js +152 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/world.js +281 -0
- package/dist/tools/world.js.map +1 -0
- package/package.json +62 -0
- package/plugin/default.project.json +6 -0
- package/plugin/src/Config.luau +59 -0
- package/plugin/src/Console.luau +657 -0
- package/plugin/src/Context.luau +35 -0
- package/plugin/src/Dispatch.luau +90 -0
- package/plugin/src/Editor.luau +142 -0
- package/plugin/src/Emulation.luau +151 -0
- package/plugin/src/LogBuffer.luau +277 -0
- package/plugin/src/Net.luau +102 -0
- package/plugin/src/Paths.luau +255 -0
- package/plugin/src/Phrase.luau +465 -0
- package/plugin/src/Png.luau +238 -0
- package/plugin/src/Scope.luau +78 -0
- package/plugin/src/ScriptEdit.luau +100 -0
- package/plugin/src/Serialize.luau +287 -0
- package/plugin/src/TextEdit.luau +296 -0
- package/plugin/src/Transport.luau +328 -0
- package/plugin/src/Undo.luau +72 -0
- package/plugin/src/Visuals.luau +710 -0
- package/plugin/src/handlers/Api.luau +242 -0
- package/plugin/src/handlers/Assets.luau +145 -0
- package/plugin/src/handlers/Capture.luau +187 -0
- package/plugin/src/handlers/Character.luau +361 -0
- package/plugin/src/handlers/Debug.luau +391 -0
- package/plugin/src/handlers/Device.luau +119 -0
- package/plugin/src/handlers/Discover.luau +289 -0
- package/plugin/src/handlers/Exec.luau +270 -0
- package/plugin/src/handlers/Geometry.luau +261 -0
- package/plugin/src/handlers/Input.luau +287 -0
- package/plugin/src/handlers/Instances.luau +389 -0
- package/plugin/src/handlers/Perf.luau +645 -0
- package/plugin/src/handlers/Playtest.luau +205 -0
- package/plugin/src/handlers/Scripts.luau +387 -0
- package/plugin/src/handlers/Session.luau +168 -0
- package/plugin/src/handlers/Viewport.luau +302 -0
- package/plugin/src/handlers/World.luau +176 -0
- package/plugin/src/init.server.luau +317 -0
- package/scripts/build-plugin.mjs +157 -0
- package/scripts/check-plugin.mjs +97 -0
- package/scripts/install-plugin.mjs +39 -0
- package/scripts/latency.mjs +201 -0
- package/scripts/locate-luau.mjs +51 -0
- package/scripts/sourcemap.mjs +58 -0
- package/scripts/test-plugin.mjs +82 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Which parts of the DataModel a search is allowed to wander into.
|
|
4
|
+
|
|
5
|
+
`game:GetChildren()` returns ~122 instances, nearly all of them engine
|
|
6
|
+
singletons like TraceRouteService and HSRDataContentProvider that no one
|
|
7
|
+
authors content into. Listing them costs thousands of tokens and buries the
|
|
8
|
+
handful of containers that matter.
|
|
9
|
+
|
|
10
|
+
A "show it if it has children" rule was tried first and let too much through:
|
|
11
|
+
MemStorageService alone reports 197 children of pure engine state. So the root
|
|
12
|
+
level is an explicit allowlist of the containers a place is actually authored
|
|
13
|
+
in. Everything else stays reachable by passing its path directly, and callers
|
|
14
|
+
report how many were hidden so that is discoverable.
|
|
15
|
+
]]
|
|
16
|
+
|
|
17
|
+
local Scope = {}
|
|
18
|
+
|
|
19
|
+
Scope.ROOT_ALWAYS = {
|
|
20
|
+
Workspace = true,
|
|
21
|
+
Players = true,
|
|
22
|
+
Lighting = true,
|
|
23
|
+
ReplicatedFirst = true,
|
|
24
|
+
ReplicatedStorage = true,
|
|
25
|
+
ServerScriptService = true,
|
|
26
|
+
ServerStorage = true,
|
|
27
|
+
StarterGui = true,
|
|
28
|
+
StarterPack = true,
|
|
29
|
+
StarterPlayer = true,
|
|
30
|
+
SoundService = true,
|
|
31
|
+
Teams = true,
|
|
32
|
+
MaterialService = true,
|
|
33
|
+
TextChatService = true,
|
|
34
|
+
Chat = true,
|
|
35
|
+
LocalizationService = true,
|
|
36
|
+
TestService = true,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
--[[
|
|
40
|
+
The top-level service an instance sits under, or the instance itself when it
|
|
41
|
+
is one. Used to hide whole branches (everything inside CoreGui) rather than
|
|
42
|
+
just the service node.
|
|
43
|
+
]]
|
|
44
|
+
function Scope.topService(instance: Instance): Instance?
|
|
45
|
+
if instance == game then
|
|
46
|
+
return nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
local current = instance
|
|
50
|
+
local parent = current.Parent
|
|
51
|
+
while parent ~= nil and parent ~= game do
|
|
52
|
+
current = parent
|
|
53
|
+
parent = current.Parent
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
-- Landing on `game` means `current` is the service. Landing on nil means the
|
|
57
|
+
-- instance is detached from the DataModel, which is not noise to filter.
|
|
58
|
+
if parent == game then
|
|
59
|
+
return current
|
|
60
|
+
end
|
|
61
|
+
return nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
function Scope.isVisibleService(service: Instance): boolean
|
|
65
|
+
return Scope.ROOT_ALWAYS[service.Name] == true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
--[[
|
|
69
|
+
True for instances the caller almost certainly did not mean to see. Only
|
|
70
|
+
applied when walking from the DataModel root; an explicit `path` into a
|
|
71
|
+
hidden service is always honoured.
|
|
72
|
+
]]
|
|
73
|
+
function Scope.isNoisy(instance: Instance): boolean
|
|
74
|
+
local service = Scope.topService(instance)
|
|
75
|
+
return service ~= nil and not Scope.isVisibleService(service)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return Scope
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Editor-safe script reading and writing.
|
|
4
|
+
|
|
5
|
+
Every other Roblox Studio MCP server assigns `script.Source` directly. That is
|
|
6
|
+
wrong in a specific, silent way: when the script is open in the Studio script
|
|
7
|
+
editor, the editor holds its own buffer, and a direct Source write either gets
|
|
8
|
+
overwritten by that buffer or discards the user's unsaved edits. Roblox added
|
|
9
|
+
`ScriptEditorService:UpdateSourceAsync` precisely to close this gap -- it
|
|
10
|
+
routes the change through the editor so the open document, its undo stack and
|
|
11
|
+
the saved source all stay in agreement.
|
|
12
|
+
|
|
13
|
+
The cost is that it yields, so every write here is async. That is the whole
|
|
14
|
+
reason handlers are allowed to yield.
|
|
15
|
+
]]
|
|
16
|
+
|
|
17
|
+
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
18
|
+
|
|
19
|
+
local Dispatch = require(script.Parent.Dispatch)
|
|
20
|
+
|
|
21
|
+
local ScriptEdit = {}
|
|
22
|
+
|
|
23
|
+
export type SourceContainer = LuaSourceContainer & { Source: string }
|
|
24
|
+
|
|
25
|
+
--[[
|
|
26
|
+
Reads current source. `GetEditorSource` is preferred over `.Source` because it
|
|
27
|
+
returns the editor's live buffer, including edits the user has typed but not
|
|
28
|
+
saved -- reading `.Source` there would hand the agent stale code and it would
|
|
29
|
+
then "fix" changes the user just made.
|
|
30
|
+
]]
|
|
31
|
+
function ScriptEdit.read(target: LuaSourceContainer): string
|
|
32
|
+
local ok, source = pcall(function()
|
|
33
|
+
return ScriptEditorService:GetEditorSource(target)
|
|
34
|
+
end)
|
|
35
|
+
if ok and typeof(source) == "string" then
|
|
36
|
+
return source
|
|
37
|
+
end
|
|
38
|
+
return (target :: SourceContainer).Source
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
--[[
|
|
42
|
+
Replaces source through the editor. `transform` receives the current text and
|
|
43
|
+
returns the replacement; returning the input unchanged is a no-op.
|
|
44
|
+
|
|
45
|
+
`transform` is allowed to raise a structured Dispatch failure -- a bad line
|
|
46
|
+
range is only discoverable once the authoritative source is in hand. Such a
|
|
47
|
+
failure is stashed rather than thrown from inside the callback: throwing
|
|
48
|
+
there would surface as an opaque engine error and lose the code and hint the
|
|
49
|
+
agent needs. The callback instead returns the text untouched, making that one
|
|
50
|
+
script a no-op, and the original failure is re-raised afterwards.
|
|
51
|
+
|
|
52
|
+
Raises SCRIPT_LOCKED when the write is refused, which normally means the
|
|
53
|
+
script is a package member or is owned by another Team Create session.
|
|
54
|
+
]]
|
|
55
|
+
function ScriptEdit.write(target: LuaSourceContainer, transform: (string) -> string)
|
|
56
|
+
local pending: any = nil
|
|
57
|
+
|
|
58
|
+
local ok, err = pcall(function()
|
|
59
|
+
ScriptEditorService:UpdateSourceAsync(target, function(source)
|
|
60
|
+
local applied, result = pcall(transform, source)
|
|
61
|
+
if applied then
|
|
62
|
+
return result
|
|
63
|
+
end
|
|
64
|
+
pending = result
|
|
65
|
+
return source
|
|
66
|
+
end)
|
|
67
|
+
end)
|
|
68
|
+
|
|
69
|
+
if pending ~= nil then
|
|
70
|
+
error(pending, 0)
|
|
71
|
+
end
|
|
72
|
+
if ok then
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
local message = tostring(err)
|
|
77
|
+
if string.find(string.lower(message), "lock") or string.find(string.lower(message), "permission") then
|
|
78
|
+
Dispatch.fail(
|
|
79
|
+
"SCRIPT_LOCKED",
|
|
80
|
+
string.format("Studio refused to edit %s: %s", target:GetFullName(), message),
|
|
81
|
+
"The script is usually locked because it belongs to a package or is "
|
|
82
|
+
.. "checked out by another Team Create user. Ask the user to unlock it, "
|
|
83
|
+
.. "or edit a different script."
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
Dispatch.fail(
|
|
87
|
+
"EDIT_FAILED",
|
|
88
|
+
string.format("Could not edit %s: %s", target:GetFullName(), message)
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
--[[
|
|
93
|
+
True for instances that hold Luau source. Used to keep script tools from
|
|
94
|
+
being pointed at ordinary instances with a confusing property error.
|
|
95
|
+
]]
|
|
96
|
+
function ScriptEdit.isScript(instance: Instance): boolean
|
|
97
|
+
return instance:IsA("LuaSourceContainer")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
return ScriptEdit
|
|
@@ -0,0 +1,287 @@
|
|
|
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 Serialize = {}
|
|
17
|
+
|
|
18
|
+
-- Floats are rounded before display: Studio reports positions like
|
|
19
|
+
-- 12.000000476837158, which costs tokens and means nothing to the caller.
|
|
20
|
+
local PRECISION = 4
|
|
21
|
+
|
|
22
|
+
-- API-dump type names that accept a plain Luau number.
|
|
23
|
+
local NUMERIC_TYPES: { [string]: boolean } = {
|
|
24
|
+
int = true,
|
|
25
|
+
int64 = true,
|
|
26
|
+
float = true,
|
|
27
|
+
double = true,
|
|
28
|
+
number = true,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
local function num(value: number): string
|
|
32
|
+
local rounded = math.round(value * 10 ^ PRECISION) / 10 ^ PRECISION
|
|
33
|
+
if rounded == math.floor(rounded) and math.abs(rounded) < 1e15 then
|
|
34
|
+
return string.format("%d", rounded)
|
|
35
|
+
end
|
|
36
|
+
return tostring(rounded)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
--[[
|
|
40
|
+
Serializes one value. Unknown userdata falls back to `tostring`, which is
|
|
41
|
+
always better than dropping the property silently.
|
|
42
|
+
]]
|
|
43
|
+
function Serialize.value(value: any): any
|
|
44
|
+
local kind = typeof(value)
|
|
45
|
+
|
|
46
|
+
if kind == "string" or kind == "boolean" or kind == "nil" then
|
|
47
|
+
return value
|
|
48
|
+
end
|
|
49
|
+
if kind == "number" then
|
|
50
|
+
-- Kept numeric so the agent can do arithmetic without parsing.
|
|
51
|
+
return math.round(value * 10 ^ PRECISION) / 10 ^ PRECISION
|
|
52
|
+
end
|
|
53
|
+
if kind == "Vector3" then
|
|
54
|
+
return string.format("%s, %s, %s", num(value.X), num(value.Y), num(value.Z))
|
|
55
|
+
end
|
|
56
|
+
if kind == "Vector2" then
|
|
57
|
+
return string.format("%s, %s", num(value.X), num(value.Y))
|
|
58
|
+
end
|
|
59
|
+
if kind == "CFrame" then
|
|
60
|
+
local position = value.Position
|
|
61
|
+
local rx, ry, rz = value:ToOrientation()
|
|
62
|
+
return string.format(
|
|
63
|
+
"pos %s, %s, %s | rot %s, %s, %s",
|
|
64
|
+
num(position.X),
|
|
65
|
+
num(position.Y),
|
|
66
|
+
num(position.Z),
|
|
67
|
+
num(math.deg(rx)),
|
|
68
|
+
num(math.deg(ry)),
|
|
69
|
+
num(math.deg(rz))
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
if kind == "Color3" then
|
|
73
|
+
return string.format("%s, %s, %s", num(value.R), num(value.G), num(value.B))
|
|
74
|
+
end
|
|
75
|
+
if kind == "BrickColor" then
|
|
76
|
+
return value.Name
|
|
77
|
+
end
|
|
78
|
+
if kind == "UDim2" then
|
|
79
|
+
return string.format(
|
|
80
|
+
"{%s, %s}, {%s, %s}",
|
|
81
|
+
num(value.X.Scale),
|
|
82
|
+
num(value.X.Offset),
|
|
83
|
+
num(value.Y.Scale),
|
|
84
|
+
num(value.Y.Offset)
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
if kind == "UDim" then
|
|
88
|
+
return string.format("{%s, %s}", num(value.Scale), num(value.Offset))
|
|
89
|
+
end
|
|
90
|
+
if kind == "EnumItem" then
|
|
91
|
+
return string.format("Enum.%s.%s", tostring(value.EnumType), value.Name)
|
|
92
|
+
end
|
|
93
|
+
if kind == "Instance" then
|
|
94
|
+
return value:GetFullName()
|
|
95
|
+
end
|
|
96
|
+
if kind == "NumberRange" then
|
|
97
|
+
return string.format("%s..%s", num(value.Min), num(value.Max))
|
|
98
|
+
end
|
|
99
|
+
if kind == "Rect" then
|
|
100
|
+
return string.format(
|
|
101
|
+
"%s, %s, %s, %s",
|
|
102
|
+
num(value.Min.X),
|
|
103
|
+
num(value.Min.Y),
|
|
104
|
+
num(value.Max.X),
|
|
105
|
+
num(value.Max.Y)
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
if kind == "ColorSequence" then
|
|
109
|
+
local parts: { string } = {}
|
|
110
|
+
for _, keypoint in value.Keypoints do
|
|
111
|
+
table.insert(
|
|
112
|
+
parts,
|
|
113
|
+
string.format("%s=%s", num(keypoint.Time), Serialize.value(keypoint.Value))
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
return table.concat(parts, " ")
|
|
117
|
+
end
|
|
118
|
+
if kind == "NumberSequence" then
|
|
119
|
+
local parts: { string } = {}
|
|
120
|
+
for _, keypoint in value.Keypoints do
|
|
121
|
+
table.insert(parts, string.format("%s=%s", num(keypoint.Time), num(keypoint.Value)))
|
|
122
|
+
end
|
|
123
|
+
return table.concat(parts, " ")
|
|
124
|
+
end
|
|
125
|
+
if kind == "table" then
|
|
126
|
+
local copy: { [any]: any } = {}
|
|
127
|
+
for key, item in value :: { [any]: any } do
|
|
128
|
+
copy[key] = Serialize.value(item)
|
|
129
|
+
end
|
|
130
|
+
return copy
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
return tostring(value)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
--[[
|
|
137
|
+
Reads a property without throwing.
|
|
138
|
+
|
|
139
|
+
Property access on an instance can error even when the API dump says the
|
|
140
|
+
property exists -- some are context-gated (only readable during a playtest,
|
|
141
|
+
or only on a loaded asset). Returning nil for those keeps one awkward
|
|
142
|
+
property from failing an inspect of 50 instances.
|
|
143
|
+
]]
|
|
144
|
+
function Serialize.readProperty(instance: Instance, name: string): (boolean, any)
|
|
145
|
+
local ok, value = pcall(function()
|
|
146
|
+
return (instance :: any)[name]
|
|
147
|
+
end)
|
|
148
|
+
if not ok then
|
|
149
|
+
return false, nil
|
|
150
|
+
end
|
|
151
|
+
return true, Serialize.value(value)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
--[[
|
|
155
|
+
Parses a serialized string back into a Roblox value, given the target type
|
|
156
|
+
from the API dump. Returns ok=false with a reason the agent can act on
|
|
157
|
+
rather than raising, so batch writes can report per-property failures.
|
|
158
|
+
]]
|
|
159
|
+
function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
160
|
+
local kind = typeof(text)
|
|
161
|
+
|
|
162
|
+
-- Values already of the right primitive shape pass straight through. The
|
|
163
|
+
-- number case is matched against the target type rather than excluded from
|
|
164
|
+
-- one: a bare 5 is a valid `float`, but not a valid Vector3, Enum or Color3.
|
|
165
|
+
if kind == "boolean" then
|
|
166
|
+
return true, text, nil
|
|
167
|
+
end
|
|
168
|
+
if kind == "number" and NUMERIC_TYPES[valueType] then
|
|
169
|
+
return true, text, nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if valueType == "string" or valueType == "Content" or valueType == "ProtectedString" then
|
|
173
|
+
return true, tostring(text), nil
|
|
174
|
+
end
|
|
175
|
+
if valueType == "bool" then
|
|
176
|
+
if kind == "boolean" then
|
|
177
|
+
return true, text, nil
|
|
178
|
+
end
|
|
179
|
+
return true, text == "true", nil
|
|
180
|
+
end
|
|
181
|
+
if NUMERIC_TYPES[valueType] then
|
|
182
|
+
local parsed = tonumber(text)
|
|
183
|
+
if not parsed then
|
|
184
|
+
return false, nil, string.format('expected a number, got "%s"', tostring(text))
|
|
185
|
+
end
|
|
186
|
+
return true, parsed, nil
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
local numbers: { number } = {}
|
|
190
|
+
if kind == "string" then
|
|
191
|
+
for match in string.gmatch(text, "-?%d+%.?%d*") do
|
|
192
|
+
table.insert(numbers, tonumber(match) :: number)
|
|
193
|
+
end
|
|
194
|
+
elseif kind == "table" then
|
|
195
|
+
for _, item in text :: { any } do
|
|
196
|
+
local parsed = tonumber(item)
|
|
197
|
+
if parsed then
|
|
198
|
+
table.insert(numbers, parsed)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if valueType == "Vector3" then
|
|
204
|
+
if #numbers < 3 then
|
|
205
|
+
return false, nil, 'expected three numbers, e.g. "12, 0, 5"'
|
|
206
|
+
end
|
|
207
|
+
return true, Vector3.new(numbers[1], numbers[2], numbers[3]), nil
|
|
208
|
+
end
|
|
209
|
+
if valueType == "Vector2" then
|
|
210
|
+
if #numbers < 2 then
|
|
211
|
+
return false, nil, 'expected two numbers, e.g. "12, 5"'
|
|
212
|
+
end
|
|
213
|
+
return true, Vector2.new(numbers[1], numbers[2]), nil
|
|
214
|
+
end
|
|
215
|
+
if valueType == "Color3" then
|
|
216
|
+
if #numbers < 3 then
|
|
217
|
+
return false, nil, 'expected three 0-1 components, e.g. "1, 0.5, 0"'
|
|
218
|
+
end
|
|
219
|
+
-- 0-255 is a common mistake and unambiguous to detect, so accept it.
|
|
220
|
+
local scale = if numbers[1] > 1 or numbers[2] > 1 or numbers[3] > 1 then 255 else 1
|
|
221
|
+
return true, Color3.new(numbers[1] / scale, numbers[2] / scale, numbers[3] / scale), nil
|
|
222
|
+
end
|
|
223
|
+
if valueType == "BrickColor" then
|
|
224
|
+
-- Cast because the API definitions type this overload as a literal union
|
|
225
|
+
-- of every BrickColor name. At runtime it takes any string and throws on
|
|
226
|
+
-- an unknown one, which is exactly what the pcall is here to catch.
|
|
227
|
+
local ok, color = pcall(function()
|
|
228
|
+
return BrickColor.new(tostring(text) :: any)
|
|
229
|
+
end)
|
|
230
|
+
if not ok then
|
|
231
|
+
return false, nil, string.format('"%s" is not a BrickColor name', tostring(text))
|
|
232
|
+
end
|
|
233
|
+
return true, color, nil
|
|
234
|
+
end
|
|
235
|
+
if valueType == "UDim2" then
|
|
236
|
+
if #numbers < 4 then
|
|
237
|
+
return false, nil, 'expected four numbers, e.g. "{0.5, 10}, {0.5, 20}"'
|
|
238
|
+
end
|
|
239
|
+
return true, UDim2.new(numbers[1], numbers[2], numbers[3], numbers[4]), nil
|
|
240
|
+
end
|
|
241
|
+
if valueType == "UDim" then
|
|
242
|
+
if #numbers < 2 then
|
|
243
|
+
return false, nil, 'expected two numbers, e.g. "{0.5, 10}"'
|
|
244
|
+
end
|
|
245
|
+
return true, UDim.new(numbers[1], numbers[2]), nil
|
|
246
|
+
end
|
|
247
|
+
if valueType == "CFrame" then
|
|
248
|
+
if #numbers >= 6 then
|
|
249
|
+
return true,
|
|
250
|
+
CFrame.new(numbers[1], numbers[2], numbers[3])
|
|
251
|
+
* CFrame.fromOrientation(
|
|
252
|
+
math.rad(numbers[4]),
|
|
253
|
+
math.rad(numbers[5]),
|
|
254
|
+
math.rad(numbers[6])
|
|
255
|
+
),
|
|
256
|
+
nil
|
|
257
|
+
end
|
|
258
|
+
if #numbers >= 3 then
|
|
259
|
+
return true, CFrame.new(numbers[1], numbers[2], numbers[3]), nil
|
|
260
|
+
end
|
|
261
|
+
return false, nil, 'expected "pos x, y, z" or "pos x, y, z | rot rx, ry, rz"'
|
|
262
|
+
end
|
|
263
|
+
if valueType == "NumberRange" then
|
|
264
|
+
if #numbers < 2 then
|
|
265
|
+
return false, nil, 'expected "min..max"'
|
|
266
|
+
end
|
|
267
|
+
return true, NumberRange.new(numbers[1], numbers[2]), nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
-- Enum values arrive as "Enum.Material.Plastic" or bare "Plastic".
|
|
271
|
+
local enumType = (Enum :: any)[valueType]
|
|
272
|
+
if enumType then
|
|
273
|
+
local name = tostring(text):match("([^%.]+)$") or tostring(text)
|
|
274
|
+
for _, item in enumType:GetEnumItems() do
|
|
275
|
+
if item.Name == name then
|
|
276
|
+
return true, item, nil
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
return false,
|
|
280
|
+
nil,
|
|
281
|
+
string.format('"%s" is not a member of Enum.%s', tostring(text), valueType)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
return false, nil, string.format('no conversion known for type "%s"', valueType)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
return Serialize
|