@el4cteo/rbx-studio-mcp 0.3.1 → 0.3.6
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 +20 -26
- package/dist/index.js +1 -1
- package/dist/lib/format.js +18 -2
- package/dist/lib/format.js.map +1 -1
- package/dist/tools/api.js +20 -1
- package/dist/tools/api.js.map +1 -1
- package/dist/tools/debug.js +18 -8
- package/dist/tools/debug.js.map +1 -1
- package/dist/tools/discover.js +5 -0
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/input.js +38 -5
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/instances.js +49 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +9 -8
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +10 -1
- package/dist/tools/scripts.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +301 -117
- package/plugin/src/Mirror.luau +126 -0
- package/plugin/src/Serialize.luau +43 -3
- package/plugin/src/ThemePicker.luau +458 -0
- package/plugin/src/Themes/Aurora.luau +161 -0
- package/plugin/src/Themes/Blueprint.luau +213 -0
- package/plugin/src/Themes/Draw.luau +177 -0
- package/plugin/src/Themes/Lattice.luau +304 -0
- package/plugin/src/Themes/Nebula.luau +182 -0
- package/plugin/src/Themes/Observatory.luau +200 -0
- package/plugin/src/Themes/Orbit.luau +268 -0
- package/plugin/src/Themes/Phosphor.luau +200 -0
- package/plugin/src/Themes/Theme.luau +121 -0
- package/plugin/src/Themes/Void.luau +266 -0
- package/plugin/src/Themes/init.luau +116 -0
- package/plugin/src/Undo.luau +29 -0
- package/plugin/src/Visuals.luau +838 -907
- package/plugin/src/handlers/Device.luau +21 -1
- package/plugin/src/handlers/Discover.luau +75 -1
- package/plugin/src/handlers/Exec.luau +17 -5
- package/plugin/src/handlers/Input.luau +583 -493
- package/plugin/src/handlers/Instances.luau +35 -3
- package/plugin/src/init.server.luau +209 -11
|
@@ -53,13 +53,45 @@ local function applyProperty(target: Instance, name: string, spec: PropertySpec)
|
|
|
53
53
|
return nil
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
--[[
|
|
57
|
+
Sets attributes, honouring an explicit type where one is given.
|
|
58
|
+
|
|
59
|
+
A bare value is written as it arrives, so a string stays a string. That is
|
|
60
|
+
the safe default and it is also a trap: attributes hold Vector3, Color3,
|
|
61
|
+
UDim2 and the rest, and `"0, 5, 0"` -- the exact text that sets a Vector3
|
|
62
|
+
property -- was landing on an attribute as five characters. The write looked
|
|
63
|
+
like it worked and the game read a string. So a value may instead arrive as
|
|
64
|
+
{ type = "Vector3", value = "0, 5, 0" }, which goes through the same parser
|
|
65
|
+
properties use.
|
|
66
|
+
]]
|
|
56
67
|
local function applyAttributes(target: Instance, attributes: { [string]: any }): { string }
|
|
57
68
|
local failures: { string } = {}
|
|
58
69
|
for name, value in attributes do
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
local resolved: any = value
|
|
71
|
+
local typed = typeof(value) == "table" and (value :: any).type ~= nil
|
|
72
|
+
if typed then
|
|
73
|
+
local spec = value :: { type: string, value: any }
|
|
74
|
+
local parsedOk, parsed, reason = Serialize.parse(spec.value, spec.type)
|
|
75
|
+
if not parsedOk then
|
|
76
|
+
table.insert(
|
|
77
|
+
failures,
|
|
78
|
+
string.format(
|
|
79
|
+
"attribute %s: %s",
|
|
80
|
+
name,
|
|
81
|
+
reason or string.format("could not be read as a %s", tostring(spec.type))
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
continue
|
|
85
|
+
end
|
|
86
|
+
resolved = parsed
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
-- An empty string means "remove this attribute", which SetAttribute spells
|
|
90
|
+
-- as nil. There is no other way to express removal in the payload -- and
|
|
91
|
+
-- the typed form is how to set an attribute to a genuinely empty string,
|
|
92
|
+
-- since only the bare form is read as removal.
|
|
61
93
|
local ok, err = pcall(function()
|
|
62
|
-
target:SetAttribute(name, if value == "" then nil else
|
|
94
|
+
target:SetAttribute(name, if not typed and value == "" then nil else resolved)
|
|
63
95
|
end)
|
|
64
96
|
if not ok then
|
|
65
97
|
table.insert(failures, string.format("attribute %s: %s", name, tostring(err)))
|
|
@@ -14,7 +14,9 @@ local Config = require(script.Config)
|
|
|
14
14
|
local Console = require(script.Console)
|
|
15
15
|
local Dispatch = require(script.Dispatch)
|
|
16
16
|
local LogBuffer = require(script.LogBuffer)
|
|
17
|
+
local Mirror = require(script.Mirror)
|
|
17
18
|
local Phrase = require(script.Phrase)
|
|
19
|
+
local Themes = require(script.Themes)
|
|
18
20
|
local Transport = require(script.Transport)
|
|
19
21
|
local Debug = require(script.handlers.Debug)
|
|
20
22
|
local Assets = require(script.handlers.Assets)
|
|
@@ -37,6 +39,9 @@ local Session = require(script.handlers.Session)
|
|
|
37
39
|
local SETTING_PORT = "port"
|
|
38
40
|
local SETTING_AUTOCONNECT = "autoConnect"
|
|
39
41
|
local SETTING_FORCE_POLL = "forcePoll"
|
|
42
|
+
local SETTING_THEME = "theme"
|
|
43
|
+
local SETTING_WIDGET_OPEN = "widgetOpen"
|
|
44
|
+
local SETTING_WIDGET_SIZE = "widgetSize"
|
|
40
45
|
|
|
41
46
|
--[[
|
|
42
47
|
A fresh id per plugin load, which in practice means one per Studio window.
|
|
@@ -85,9 +90,10 @@ local function canConnect(): (boolean, string?)
|
|
|
85
90
|
]]
|
|
86
91
|
return false,
|
|
87
92
|
"client view of a playtest -- Studio forbids client sessions from making HTTP "
|
|
88
|
-
.. "requests
|
|
89
|
-
.. "
|
|
90
|
-
.. "
|
|
93
|
+
.. "requests, so this panel cannot reach the bridge itself. It is MIRRORING "
|
|
94
|
+
.. "the playtest's server session instead, so the log and the strip below "
|
|
95
|
+
.. "are live. Switch to the Server view (Test tab, Current: Server) for the "
|
|
96
|
+
.. "session that is actually connected."
|
|
91
97
|
end
|
|
92
98
|
|
|
93
99
|
-- First thing, before any handler or the transport can log: the buffer only
|
|
@@ -103,6 +109,36 @@ end
|
|
|
103
109
|
|
|
104
110
|
Transport.setForcePoll(plugin:GetSetting(SETTING_FORCE_POLL) == true)
|
|
105
111
|
|
|
112
|
+
--[[
|
|
113
|
+
The console's colour preset, restored before anything is drawn.
|
|
114
|
+
|
|
115
|
+
Applied here rather than after mounting so the panel is built in the right
|
|
116
|
+
palette from the start -- restoring it afterwards would flash the default
|
|
117
|
+
theme for a frame on every Studio launch. An unknown id (a preset renamed,
|
|
118
|
+
or a setting written by a newer build) falls back to the default rather than
|
|
119
|
+
failing, which is `Themes.get`'s job.
|
|
120
|
+
]]
|
|
121
|
+
local storedTheme = plugin:GetSetting(SETTING_THEME)
|
|
122
|
+
if typeof(storedTheme) == "string" then
|
|
123
|
+
Themes.use(storedTheme)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
--[[
|
|
127
|
+
Whether the restore above still has to be pushed into the console.
|
|
128
|
+
|
|
129
|
+
`Themes.use` moves the ACTIVE ID, and anything that reads the palette live
|
|
130
|
+
picks the change up for free -- which is why the prism cell came back on the
|
|
131
|
+
saved preset. `Console` does not read it live: it caches the palette in an
|
|
132
|
+
upvalue at module load, deliberately, so that forty read sites stay plain
|
|
133
|
+
field accesses. Module load happens at the `require` above, which is BEFORE
|
|
134
|
+
this line, so the cache held the default while the id said otherwise, and a
|
|
135
|
+
reload came back as the saved prism drawn in the default's colours.
|
|
136
|
+
|
|
137
|
+
It cannot simply be applied here either -- the console is not mounted yet.
|
|
138
|
+
So it is remembered and applied the moment it can be, right after mounting.
|
|
139
|
+
]]
|
|
140
|
+
local themeNeedsApplying = typeof(storedTheme) == "string" and Themes.activeId() == storedTheme
|
|
141
|
+
|
|
106
142
|
Session.register()
|
|
107
143
|
Discover.register()
|
|
108
144
|
Debug.register()
|
|
@@ -121,23 +157,131 @@ Input.register()
|
|
|
121
157
|
Device.register()
|
|
122
158
|
Api.register()
|
|
123
159
|
|
|
160
|
+
--[[
|
|
161
|
+
The toolbar button.
|
|
162
|
+
|
|
163
|
+
The icon has to be an uploaded asset: `CreateButton` takes a content string,
|
|
164
|
+
and the only ones Studio resolves are `rbxassetid://` for uploaded images and
|
|
165
|
+
`rbxasset://` for files that ship inside Studio itself. A path to something in
|
|
166
|
+
this repository is not one of them, which is why the mark in `assets/` has to
|
|
167
|
+
go through an upload before it can appear here.
|
|
168
|
+
|
|
169
|
+
It used to borrow `textures/ui/common/robux.png` -- a Robux coin, sitting in
|
|
170
|
+
the toolbar next to a plugin that has nothing to do with purchases.
|
|
171
|
+
]]
|
|
124
172
|
local toolbar = plugin:CreateToolbar("rbx-studio")
|
|
125
173
|
local button = toolbar:CreateButton(
|
|
126
174
|
"rbx-studio",
|
|
127
175
|
"Show the rbx-studio console",
|
|
128
|
-
"
|
|
176
|
+
"rbxassetid://125390773465346"
|
|
129
177
|
)
|
|
130
178
|
button.ClickableWhenViewportHidden = true
|
|
131
179
|
|
|
132
|
-
--
|
|
133
|
-
|
|
134
|
-
|
|
180
|
+
--[[
|
|
181
|
+
The panel, opened and sized the way the user last left it -- in EVERY
|
|
182
|
+
DataModel, which is the whole point.
|
|
183
|
+
|
|
184
|
+
Pressing Play loads this plugin again into the playtest's DataModels, and a
|
|
185
|
+
dock widget belongs to the DataModel that created it: the editor's is hidden
|
|
186
|
+
along with the editor's view, and the playtest's is a brand new widget that
|
|
187
|
+
Studio brings up closed and at the default size. So the panel vanished on
|
|
188
|
+
every playtest and came back, when the user re-opened it from the toolbar,
|
|
189
|
+
as a 560x320 float with whatever size they had chosen thrown away.
|
|
190
|
+
|
|
191
|
+
Studio's own restore does not cross that boundary, so the preference is kept
|
|
192
|
+
here instead and passed in as the INITIAL state, with `overrideEnabledRestore`
|
|
193
|
+
set so it is honoured rather than second-guessed. Written only from the
|
|
194
|
+
editor session -- see below -- so a playtest starting or ending can never
|
|
195
|
+
record a decision the user did not make.
|
|
196
|
+
]]
|
|
197
|
+
local storedOpen = plugin:GetSetting(SETTING_WIDGET_OPEN)
|
|
198
|
+
local wantOpen = if typeof(storedOpen) == "boolean" then storedOpen else true
|
|
199
|
+
|
|
200
|
+
local floatWidth, floatHeight = 560, 320
|
|
201
|
+
local storedSize = plugin:GetSetting(SETTING_WIDGET_SIZE)
|
|
202
|
+
if typeof(storedSize) == "table" then
|
|
203
|
+
local saved = storedSize :: { [string]: any }
|
|
204
|
+
local x, y = tonumber(saved.x), tonumber(saved.y)
|
|
205
|
+
-- Guarded against nonsense: a zero or absurd size saved from a docked or
|
|
206
|
+
-- mid-teardown widget would otherwise be unrecoverable without clearing
|
|
207
|
+
-- settings by hand.
|
|
208
|
+
if x ~= nil and y ~= nil and x >= 360 and y >= 200 and x <= 4000 and y <= 4000 then
|
|
209
|
+
floatWidth, floatHeight = math.floor(x), math.floor(y)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
135
213
|
local widget = plugin:CreateDockWidgetPluginGuiAsync(
|
|
136
214
|
"StudioMCP_Console",
|
|
137
|
-
DockWidgetPluginGuiInfo.new(
|
|
215
|
+
DockWidgetPluginGuiInfo.new(
|
|
216
|
+
Enum.InitialDockState.Float,
|
|
217
|
+
wantOpen,
|
|
218
|
+
-- Override Studio's own enabled-restore: ours is the one that survives
|
|
219
|
+
-- the hop into a playtest DataModel, and two restores disagreeing is
|
|
220
|
+
-- what produced a panel that was open in the editor and closed in play.
|
|
221
|
+
true,
|
|
222
|
+
floatWidth,
|
|
223
|
+
floatHeight,
|
|
224
|
+
360,
|
|
225
|
+
200
|
|
226
|
+
)
|
|
138
227
|
)
|
|
139
228
|
widget.Title = "rbx-studio"
|
|
140
229
|
|
|
230
|
+
--[[
|
|
231
|
+
Remember what the user does with the panel, from the editor only.
|
|
232
|
+
|
|
233
|
+
The editor session is the one whose Enabled and size changes are actually
|
|
234
|
+
the user's: a playtest DataModel's widget is created, shown and destroyed by
|
|
235
|
+
Studio around the test, and letting those transitions write would persist a
|
|
236
|
+
"closed" the user never asked for -- reintroducing the bug through the back
|
|
237
|
+
door.
|
|
238
|
+
]]
|
|
239
|
+
if RunService:IsEdit() then
|
|
240
|
+
local unloading = false
|
|
241
|
+
plugin.Unloading:Connect(function()
|
|
242
|
+
unloading = true
|
|
243
|
+
end)
|
|
244
|
+
|
|
245
|
+
widget:GetPropertyChangedSignal("Enabled"):Connect(function()
|
|
246
|
+
if not unloading then
|
|
247
|
+
plugin:SetSetting(SETTING_WIDGET_OPEN, widget.Enabled)
|
|
248
|
+
end
|
|
249
|
+
end)
|
|
250
|
+
|
|
251
|
+
-- Only a real, sane size, and only while the panel is up: a hidden or
|
|
252
|
+
-- collapsing widget reports sizes that are not a choice.
|
|
253
|
+
local function persistSize(): boolean
|
|
254
|
+
local size = widget.AbsoluteSize
|
|
255
|
+
if unloading or not widget.Enabled or size.X < 360 or size.Y < 200 then
|
|
256
|
+
return false
|
|
257
|
+
end
|
|
258
|
+
plugin:SetSetting(SETTING_WIDGET_SIZE, { x = math.floor(size.X), y = math.floor(size.Y) })
|
|
259
|
+
return true
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
widget:GetPropertyChangedSignal("AbsoluteSize"):Connect(persistSize)
|
|
263
|
+
|
|
264
|
+
--[[
|
|
265
|
+
Seed from the size Studio has already restored, rather than waiting for
|
|
266
|
+
a resize that may never come.
|
|
267
|
+
|
|
268
|
+
Without this the setting stays empty until the user happens to drag the
|
|
269
|
+
panel's edge, so the first playtest inherits the 560x320 default and the
|
|
270
|
+
window visibly shrinks -- which is the same complaint as it vanishing,
|
|
271
|
+
one step later. Studio settles the geometry a few frames after the widget
|
|
272
|
+
is created and fires no change event for it, so it is polled briefly and
|
|
273
|
+
then left alone.
|
|
274
|
+
]]
|
|
275
|
+
task.defer(function()
|
|
276
|
+
for _ = 1, 40 do
|
|
277
|
+
if persistSize() then
|
|
278
|
+
return
|
|
279
|
+
end
|
|
280
|
+
task.wait(0.1)
|
|
281
|
+
end
|
|
282
|
+
end)
|
|
283
|
+
end
|
|
284
|
+
|
|
141
285
|
local currentStatus: Transport.Status = "disconnected"
|
|
142
286
|
|
|
143
287
|
local function refreshMeta()
|
|
@@ -165,8 +309,62 @@ Console.mount(widget, {
|
|
|
165
309
|
onClear = function()
|
|
166
310
|
Console.clear()
|
|
167
311
|
end,
|
|
312
|
+
onTheme = function(id)
|
|
313
|
+
plugin:SetSetting(SETTING_THEME, id)
|
|
314
|
+
Console.log("dim", string.format("theme: %s", id))
|
|
315
|
+
end,
|
|
168
316
|
})
|
|
169
317
|
|
|
318
|
+
-- The saved preset, now that there is something to paint. Done immediately
|
|
319
|
+
-- after mounting and before the first line is logged, so nothing is ever drawn
|
|
320
|
+
-- in the wrong palette.
|
|
321
|
+
if themeNeedsApplying then
|
|
322
|
+
Console.applyTheme()
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
--[[
|
|
326
|
+
The playtest halves, joined.
|
|
327
|
+
|
|
328
|
+
The server half can reach the bridge and sees every command; the client half
|
|
329
|
+
is the one Studio actually shows during a playtest and can reach nothing. So
|
|
330
|
+
the server relays its console events and the client replays them, which is
|
|
331
|
+
the only way the panel in front of the user reacts to the work being done.
|
|
332
|
+
|
|
333
|
+
Note which side sets the observer: only the SERVER. The client replays
|
|
334
|
+
events through the same `Console` functions, and if it were also observing
|
|
335
|
+
it would echo each one straight back into the channel.
|
|
336
|
+
]]
|
|
337
|
+
if Mirror.isPlaytestServer() then
|
|
338
|
+
Mirror.startServer()
|
|
339
|
+
Console.setObserver(function(kind, arguments)
|
|
340
|
+
Mirror.send(kind, arguments)
|
|
341
|
+
end)
|
|
342
|
+
elseif Mirror.isPlaytestClient() then
|
|
343
|
+
Console.setMirroring(true)
|
|
344
|
+
Mirror.startClient(function(kind, arguments)
|
|
345
|
+
if kind == "log" then
|
|
346
|
+
local level, message, detail = arguments[1], arguments[2], arguments[3]
|
|
347
|
+
if typeof(level) == "string" and typeof(message) == "string" then
|
|
348
|
+
Console.log(
|
|
349
|
+
level :: any,
|
|
350
|
+
message,
|
|
351
|
+
if typeof(detail) == "string" then detail else nil
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
elseif kind == "beginCall" then
|
|
355
|
+
local title, callKind = arguments[1], arguments[2]
|
|
356
|
+
if typeof(title) == "string" and typeof(callKind) == "string" then
|
|
357
|
+
Console.beginCall(title, callKind)
|
|
358
|
+
end
|
|
359
|
+
elseif kind == "recordCall" then
|
|
360
|
+
local ok, milliseconds = arguments[1], arguments[2]
|
|
361
|
+
if typeof(ok) == "boolean" and typeof(milliseconds) == "number" then
|
|
362
|
+
Console.recordCall(ok, milliseconds)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end)
|
|
366
|
+
end
|
|
367
|
+
|
|
170
368
|
Console.log("info", string.format("rbx-studio v%s", Config.PLUGIN_VERSION), "build " .. Config.BUILD_ID)
|
|
171
369
|
Console.log("dim", string.format("place: %s (%d)", game.Name, game.PlaceId))
|
|
172
370
|
|
|
@@ -278,12 +476,12 @@ function connect()
|
|
|
278
476
|
-- Reported as standby rather than an error: nothing failed, and this
|
|
279
477
|
-- session was never going to connect.
|
|
280
478
|
currentStatus = "disconnected"
|
|
281
|
-
Console.log("dim", "
|
|
479
|
+
Console.log("dim", "mirroring", reason)
|
|
282
480
|
Console.setStatus(
|
|
283
|
-
"
|
|
481
|
+
"mirroring",
|
|
284
482
|
string.format("%s build %s", "client view", Config.BUILD_ID)
|
|
285
483
|
)
|
|
286
|
-
Console.setCaption("
|
|
484
|
+
Console.setCaption("mirroring the playtest server")
|
|
287
485
|
return
|
|
288
486
|
end
|
|
289
487
|
Transport.start(studioId(), { onCommand = onCommand, onStatus = onStatus, onEvent = onEvent })
|