@el4cteo/rbx-studio-mcp 0.3.0 → 0.3.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/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/lib/pluginbuild.js +23 -13
- package/dist/lib/pluginbuild.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/instances.js +49 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +11 -1
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +27 -3
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/session.js +16 -0
- package/dist/tools/session.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +257 -116
- package/plugin/src/Paths.luau +138 -69
- package/plugin/src/Serialize.luau +43 -3
- package/plugin/src/ThemePicker.luau +458 -0
- package/plugin/src/Themes/Aurora.luau +147 -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 +230 -0
- package/plugin/src/Themes/init.luau +116 -0
- package/plugin/src/Visuals.luau +788 -907
- package/plugin/src/handlers/Discover.luau +75 -1
- package/plugin/src/handlers/Exec.luau +17 -5
- package/plugin/src/handlers/Instances.luau +35 -3
- package/plugin/src/handlers/Scripts.luau +58 -2
- package/plugin/src/init.server.luau +386 -354
|
@@ -1,354 +1,386 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
rbx-studio -- plugin entry point.
|
|
4
|
-
|
|
5
|
-
Owns the toolbar UI, this window's Studio identity, and the command loop.
|
|
6
|
-
Handlers do the actual work; this file only wires them to the transport and
|
|
7
|
-
reports what is happening to the console widget.
|
|
8
|
-
]]
|
|
9
|
-
|
|
10
|
-
local HttpService = game:GetService("HttpService")
|
|
11
|
-
local RunService = game:GetService("RunService")
|
|
12
|
-
|
|
13
|
-
local Config = require(script.Config)
|
|
14
|
-
local Console = require(script.Console)
|
|
15
|
-
local Dispatch = require(script.Dispatch)
|
|
16
|
-
local LogBuffer = require(script.LogBuffer)
|
|
17
|
-
local Phrase = require(script.Phrase)
|
|
18
|
-
local
|
|
19
|
-
local
|
|
20
|
-
local
|
|
21
|
-
local
|
|
22
|
-
local
|
|
23
|
-
local
|
|
24
|
-
local
|
|
25
|
-
local
|
|
26
|
-
local
|
|
27
|
-
local
|
|
28
|
-
local
|
|
29
|
-
local
|
|
30
|
-
local
|
|
31
|
-
local
|
|
32
|
-
local
|
|
33
|
-
local
|
|
34
|
-
local
|
|
35
|
-
local
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
local
|
|
39
|
-
local
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
local
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.. "
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
--
|
|
96
|
-
--
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
end
|
|
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
|
-
Console.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
rbx-studio -- plugin entry point.
|
|
4
|
+
|
|
5
|
+
Owns the toolbar UI, this window's Studio identity, and the command loop.
|
|
6
|
+
Handlers do the actual work; this file only wires them to the transport and
|
|
7
|
+
reports what is happening to the console widget.
|
|
8
|
+
]]
|
|
9
|
+
|
|
10
|
+
local HttpService = game:GetService("HttpService")
|
|
11
|
+
local RunService = game:GetService("RunService")
|
|
12
|
+
|
|
13
|
+
local Config = require(script.Config)
|
|
14
|
+
local Console = require(script.Console)
|
|
15
|
+
local Dispatch = require(script.Dispatch)
|
|
16
|
+
local LogBuffer = require(script.LogBuffer)
|
|
17
|
+
local Phrase = require(script.Phrase)
|
|
18
|
+
local Themes = require(script.Themes)
|
|
19
|
+
local Transport = require(script.Transport)
|
|
20
|
+
local Debug = require(script.handlers.Debug)
|
|
21
|
+
local Assets = require(script.handlers.Assets)
|
|
22
|
+
local Capture = require(script.handlers.Capture)
|
|
23
|
+
local Character = require(script.handlers.Character)
|
|
24
|
+
local Geometry = require(script.handlers.Geometry)
|
|
25
|
+
local World = require(script.handlers.World)
|
|
26
|
+
local Discover = require(script.handlers.Discover)
|
|
27
|
+
local Exec = require(script.handlers.Exec)
|
|
28
|
+
local Instances = require(script.handlers.Instances)
|
|
29
|
+
local Perf = require(script.handlers.Perf)
|
|
30
|
+
local Playtest = require(script.handlers.Playtest)
|
|
31
|
+
local Viewport = require(script.handlers.Viewport)
|
|
32
|
+
local Input = require(script.handlers.Input)
|
|
33
|
+
local Device = require(script.handlers.Device)
|
|
34
|
+
local Api = require(script.handlers.Api)
|
|
35
|
+
local Scripts = require(script.handlers.Scripts)
|
|
36
|
+
local Session = require(script.handlers.Session)
|
|
37
|
+
|
|
38
|
+
local SETTING_PORT = "port"
|
|
39
|
+
local SETTING_AUTOCONNECT = "autoConnect"
|
|
40
|
+
local SETTING_FORCE_POLL = "forcePoll"
|
|
41
|
+
local SETTING_THEME = "theme"
|
|
42
|
+
|
|
43
|
+
--[[
|
|
44
|
+
A fresh id per plugin load, which in practice means one per Studio window.
|
|
45
|
+
|
|
46
|
+
This was originally persisted with `plugin:SetSetting`, on the reasoning that a
|
|
47
|
+
stable id keeps reconnects mapping to the same session. That is wrong as soon
|
|
48
|
+
as the user opens a second window: plugin settings live in one file shared by
|
|
49
|
+
every Studio process, so both windows announce the same id, and the server
|
|
50
|
+
treats the second connection as the first one reconnecting -- closing the
|
|
51
|
+
original stream and making it impossible to address the two places separately.
|
|
52
|
+
|
|
53
|
+
Held in memory instead. A reconnect within one load (the SSE stream hits its
|
|
54
|
+
30-minute cap) reuses this id, and `plugin.Unloading` detaches cleanly on
|
|
55
|
+
reload, so ghost entries do not accumulate.
|
|
56
|
+
]]
|
|
57
|
+
local SESSION_ID = HttpService:GenerateGUID(false)
|
|
58
|
+
|
|
59
|
+
local function studioId(): string
|
|
60
|
+
return SESSION_ID
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
--[[
|
|
64
|
+
Whether this copy of the plugin can reach the bridge at all.
|
|
65
|
+
|
|
66
|
+
Pressing Play loads the plugin into the playtest's DataModels as well as the
|
|
67
|
+
editor's, and HttpService refuses every request from a client one: "Http
|
|
68
|
+
requests can only be executed by game server". The transport read that as a
|
|
69
|
+
dropped connection and retried forever, filling the console with red while
|
|
70
|
+
nothing was actually wrong.
|
|
71
|
+
|
|
72
|
+
The editor session and the play session's server can both connect and are
|
|
73
|
+
worth connecting -- addressing a running server is useful. The client half
|
|
74
|
+
simply says so once and stops.
|
|
75
|
+
]]
|
|
76
|
+
local function canConnect(): (boolean, string?)
|
|
77
|
+
if RunService:IsEdit() or RunService:IsServer() then
|
|
78
|
+
return true, nil
|
|
79
|
+
end
|
|
80
|
+
--[[
|
|
81
|
+
The wording names the fix, because the symptom is indistinguishable
|
|
82
|
+
from a broken server: a user watching this window during a playtest
|
|
83
|
+
sees a console that logs nothing while tools plainly work, and has no
|
|
84
|
+
way to guess that the activity is in a different view of the same
|
|
85
|
+
Studio. Reported once here and again as the strip's caption, since
|
|
86
|
+
one line scrolled off the top is easy to miss.
|
|
87
|
+
]]
|
|
88
|
+
return false,
|
|
89
|
+
"client view of a playtest -- Studio forbids client sessions from making HTTP "
|
|
90
|
+
.. "requests. The playtest's server session is connected and handling this "
|
|
91
|
+
.. "place; switch Studio to the Server view (Test tab, Current: Server) to "
|
|
92
|
+
.. "watch it work."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
-- First thing, before any handler or the transport can log: the buffer only
|
|
96
|
+
-- holds what was printed after it subscribed, so every line ahead of this call
|
|
97
|
+
-- is unrecoverable. This runs even in a client session that will never connect,
|
|
98
|
+
-- since the console tool reads it and connectivity is a separate question.
|
|
99
|
+
LogBuffer.start()
|
|
100
|
+
|
|
101
|
+
local storedPort = plugin:GetSetting(SETTING_PORT)
|
|
102
|
+
if typeof(storedPort) == "number" then
|
|
103
|
+
Config.setPort(storedPort)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
Transport.setForcePoll(plugin:GetSetting(SETTING_FORCE_POLL) == true)
|
|
107
|
+
|
|
108
|
+
--[[
|
|
109
|
+
The console's colour preset, restored before anything is drawn.
|
|
110
|
+
|
|
111
|
+
Applied here rather than after mounting so the panel is built in the right
|
|
112
|
+
palette from the start -- restoring it afterwards would flash the default
|
|
113
|
+
theme for a frame on every Studio launch. An unknown id (a preset renamed,
|
|
114
|
+
or a setting written by a newer build) falls back to the default rather than
|
|
115
|
+
failing, which is `Themes.get`'s job.
|
|
116
|
+
]]
|
|
117
|
+
local storedTheme = plugin:GetSetting(SETTING_THEME)
|
|
118
|
+
if typeof(storedTheme) == "string" then
|
|
119
|
+
Themes.use(storedTheme)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
Session.register()
|
|
123
|
+
Discover.register()
|
|
124
|
+
Debug.register()
|
|
125
|
+
Instances.register()
|
|
126
|
+
Perf.register(plugin)
|
|
127
|
+
Playtest.register()
|
|
128
|
+
Capture.register()
|
|
129
|
+
Assets.register()
|
|
130
|
+
Character.register()
|
|
131
|
+
Geometry.register()
|
|
132
|
+
World.register()
|
|
133
|
+
Exec.register()
|
|
134
|
+
Viewport.register()
|
|
135
|
+
Scripts.register()
|
|
136
|
+
Input.register()
|
|
137
|
+
Device.register()
|
|
138
|
+
Api.register()
|
|
139
|
+
|
|
140
|
+
--[[
|
|
141
|
+
The toolbar button.
|
|
142
|
+
|
|
143
|
+
The icon has to be an uploaded asset: `CreateButton` takes a content string,
|
|
144
|
+
and the only ones Studio resolves are `rbxassetid://` for uploaded images and
|
|
145
|
+
`rbxasset://` for files that ship inside Studio itself. A path to something in
|
|
146
|
+
this repository is not one of them, which is why the mark in `assets/` has to
|
|
147
|
+
go through an upload before it can appear here.
|
|
148
|
+
|
|
149
|
+
It used to borrow `textures/ui/common/robux.png` -- a Robux coin, sitting in
|
|
150
|
+
the toolbar next to a plugin that has nothing to do with purchases.
|
|
151
|
+
]]
|
|
152
|
+
local toolbar = plugin:CreateToolbar("rbx-studio")
|
|
153
|
+
local button = toolbar:CreateButton(
|
|
154
|
+
"rbx-studio",
|
|
155
|
+
"Show the rbx-studio console",
|
|
156
|
+
"rbxassetid://125390773465346"
|
|
157
|
+
)
|
|
158
|
+
button.ClickableWhenViewportHidden = true
|
|
159
|
+
|
|
160
|
+
-- Enabled by default: the whole point is that opening a place shows you it
|
|
161
|
+
-- connected without touching anything. Studio remembers the user's choice after
|
|
162
|
+
-- the first time they close it.
|
|
163
|
+
local widget = plugin:CreateDockWidgetPluginGuiAsync(
|
|
164
|
+
"StudioMCP_Console",
|
|
165
|
+
DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, true, false, 560, 320, 360, 200)
|
|
166
|
+
)
|
|
167
|
+
widget.Title = "rbx-studio"
|
|
168
|
+
|
|
169
|
+
local currentStatus: Transport.Status = "disconnected"
|
|
170
|
+
|
|
171
|
+
local function refreshMeta()
|
|
172
|
+
Console.setStatus(
|
|
173
|
+
currentStatus,
|
|
174
|
+
string.format(
|
|
175
|
+
"127.0.0.1:%d %s build %s",
|
|
176
|
+
Config.getPort(),
|
|
177
|
+
Transport.getMode(),
|
|
178
|
+
Config.BUILD_ID
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
button:SetActive(currentStatus == "connected")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
local connect: () -> ()
|
|
185
|
+
|
|
186
|
+
Console.mount(widget, {
|
|
187
|
+
onReconnect = function()
|
|
188
|
+
Console.log("info", "reconnect requested")
|
|
189
|
+
Transport.stop()
|
|
190
|
+
task.wait(0.2)
|
|
191
|
+
connect()
|
|
192
|
+
end,
|
|
193
|
+
onClear = function()
|
|
194
|
+
Console.clear()
|
|
195
|
+
end,
|
|
196
|
+
onTheme = function(id)
|
|
197
|
+
plugin:SetSetting(SETTING_THEME, id)
|
|
198
|
+
Console.log("dim", string.format("theme: %s", id))
|
|
199
|
+
end,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
Console.log("info", string.format("rbx-studio v%s", Config.PLUGIN_VERSION), "build " .. Config.BUILD_ID)
|
|
203
|
+
Console.log("dim", string.format("place: %s (%d)", game.Name, game.PlaceId))
|
|
204
|
+
|
|
205
|
+
local function onStatus(status: Transport.Status, detail: string?)
|
|
206
|
+
local previous = currentStatus
|
|
207
|
+
currentStatus = status
|
|
208
|
+
refreshMeta()
|
|
209
|
+
|
|
210
|
+
-- Only narrate transitions. The transport re-reports its state on every
|
|
211
|
+
-- reconnect attempt, and echoing an unchanged status would bury real events.
|
|
212
|
+
if status == previous and detail == nil then
|
|
213
|
+
return
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if status == "connected" then
|
|
217
|
+
Console.log(
|
|
218
|
+
"ok",
|
|
219
|
+
string.format("connected to 127.0.0.1:%d", Config.getPort()),
|
|
220
|
+
if detail then "(" .. detail .. ")" else nil
|
|
221
|
+
)
|
|
222
|
+
elseif status == "connecting" then
|
|
223
|
+
Console.log("dim", string.format("connecting to 127.0.0.1:%d...", Config.getPort()))
|
|
224
|
+
else
|
|
225
|
+
Console.log("error", "disconnected", detail)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
--[[
|
|
230
|
+
Runs each command on its own task. Handlers may yield -- `UpdateSourceAsync`
|
|
231
|
+
and any `*Async` call does -- and serialising them would let one slow edit
|
|
232
|
+
stall every other request on the stream.
|
|
233
|
+
]]
|
|
234
|
+
local function onCommand(id: string, op: string, params: { [string]: any }?)
|
|
235
|
+
task.spawn(function()
|
|
236
|
+
local startedAt = os.clock()
|
|
237
|
+
--[[
|
|
238
|
+
Named for what it does, not for how it travels. `script.edit` on
|
|
239
|
+
ServerScriptService.Systems.KillBrick reads as "Edit KillBrick",
|
|
240
|
+
which is the thing someone watching actually wants to know; the wire
|
|
241
|
+
name is kept alongside so the log still maps onto the protocol when
|
|
242
|
+
something needs debugging.
|
|
243
|
+
]]
|
|
244
|
+
local title = Phrase.of(op, params)
|
|
245
|
+
--[[
|
|
246
|
+
Announced synchronously, unlike the logging below.
|
|
247
|
+
|
|
248
|
+
Deferring this looked like free latency and was not. `beginCall` sets
|
|
249
|
+
two labels and some fields -- it never touches the RichText log, which
|
|
250
|
+
is where the cost actually is -- and deferring it let a call that
|
|
251
|
+
finished inside one frame record its result before the call had been
|
|
252
|
+
announced, so the bar on the trace lost the name it was supposed to
|
|
253
|
+
carry. A microsecond is not worth an ordering hazard.
|
|
254
|
+
]]
|
|
255
|
+
Console.beginCall(title, Phrase.kindOf(op))
|
|
256
|
+
|
|
257
|
+
local result = Dispatch.invoke(id, op, params)
|
|
258
|
+
local milliseconds = (os.clock() - startedAt) * 1000
|
|
259
|
+
|
|
260
|
+
--[[
|
|
261
|
+
The answer goes out before the console hears about it.
|
|
262
|
+
|
|
263
|
+
This used to be the last line of the function, which put a
|
|
264
|
+
`table.concat` of three hundred strings, a RichText relayout of the
|
|
265
|
+
whole log, two `Instance.new` calls and a forty-bar relayout in front
|
|
266
|
+
of the reply on its way back to the agent. None of that is work the
|
|
267
|
+
caller asked for, and all of it was being billed to the round trip
|
|
268
|
+
this project measures. Drawing happens below, on time the agent is no
|
|
269
|
+
longer waiting for.
|
|
270
|
+
]]
|
|
271
|
+
Transport.sendResult(result)
|
|
272
|
+
|
|
273
|
+
local elapsed = string.format("%.0fms", milliseconds)
|
|
274
|
+
Console.recordCall(result.ok, milliseconds)
|
|
275
|
+
|
|
276
|
+
if result.ok then
|
|
277
|
+
Console.log("reply", title, elapsed)
|
|
278
|
+
else
|
|
279
|
+
local err = result.error
|
|
280
|
+
Console.log(
|
|
281
|
+
"error",
|
|
282
|
+
string.format("%s failed: %s", title, if err then err.code else "unknown"),
|
|
283
|
+
elapsed
|
|
284
|
+
)
|
|
285
|
+
if err and err.message then
|
|
286
|
+
Console.log("dim", " " .. err.message)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
--[[
|
|
293
|
+
Bridge news that is not a command.
|
|
294
|
+
|
|
295
|
+
Kept deliberately narrow: an unknown event is ignored rather than logged,
|
|
296
|
+
because a newer server talking to an older plugin is a supported situation
|
|
297
|
+
and "unknown event" rows would be the only symptom of it working correctly.
|
|
298
|
+
]]
|
|
299
|
+
local function onEvent(event: { [string]: any })
|
|
300
|
+
if event.event == "clients" and typeof(event.count) == "number" then
|
|
301
|
+
Console.setClients(event.count)
|
|
302
|
+
elseif event.event == "agent" and event.state == "finished" then
|
|
303
|
+
Console.agentFinished()
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
function connect()
|
|
308
|
+
local allowed, reason = canConnect()
|
|
309
|
+
if not allowed then
|
|
310
|
+
-- Reported as standby rather than an error: nothing failed, and this
|
|
311
|
+
-- session was never going to connect.
|
|
312
|
+
currentStatus = "disconnected"
|
|
313
|
+
Console.log("dim", "standby", reason)
|
|
314
|
+
Console.setStatus(
|
|
315
|
+
"standby",
|
|
316
|
+
string.format("%s build %s", "client view", Config.BUILD_ID)
|
|
317
|
+
)
|
|
318
|
+
Console.setCaption("switch to the Server view to watch this playtest")
|
|
319
|
+
return
|
|
320
|
+
end
|
|
321
|
+
Transport.start(studioId(), { onCommand = onCommand, onStatus = onStatus, onEvent = onEvent })
|
|
322
|
+
plugin:SetSetting(SETTING_AUTOCONNECT, true)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
button.Click:Connect(function()
|
|
326
|
+
widget.Enabled = not widget.Enabled
|
|
327
|
+
end)
|
|
328
|
+
|
|
329
|
+
plugin.Unloading:Connect(function()
|
|
330
|
+
Transport.stop()
|
|
331
|
+
end)
|
|
332
|
+
|
|
333
|
+
--[[
|
|
334
|
+
Switches this session between the push and long-poll transports.
|
|
335
|
+
|
|
336
|
+
Registered here rather than in a handler module because it is the only
|
|
337
|
+
command that has to reach back into the plugin object and the connection
|
|
338
|
+
loop, both of which live in this file -- and registered THIS far down the
|
|
339
|
+
file on purpose: `connect` is a forward-declared local, so a closure written
|
|
340
|
+
above its declaration captures the global of that name instead, which is nil.
|
|
341
|
+
That cost a session. The plugin loaded and connected perfectly, and then died
|
|
342
|
+
the first time somebody asked it to switch transport.
|
|
343
|
+
|
|
344
|
+
The reply is sent before the reconnect, and the reconnect is deferred:
|
|
345
|
+
tearing the stream down inside the handler would strand the answer to the
|
|
346
|
+
very call that asked for the switch, which is a confusing way to succeed.
|
|
347
|
+
]]
|
|
348
|
+
Dispatch.registerAll("studio", {
|
|
349
|
+
transport = function(params: { [string]: any }): { [string]: any }
|
|
350
|
+
local requested = params.mode
|
|
351
|
+
if requested ~= nil and requested ~= "sse" and requested ~= "poll" then
|
|
352
|
+
Dispatch.fail("BAD_PARAMS", 'transport mode must be "sse" or "poll".')
|
|
353
|
+
end
|
|
354
|
+
if requested == nil then
|
|
355
|
+
return { mode = Transport.getMode(), forcePoll = Transport.getForcePoll(), changed = false }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
local wantPoll = requested == "poll"
|
|
359
|
+
if Transport.getMode() == requested and Transport.getForcePoll() == wantPoll then
|
|
360
|
+
return { mode = Transport.getMode(), forcePoll = wantPoll, changed = false }
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
Transport.setForcePoll(wantPoll)
|
|
364
|
+
plugin:SetSetting(SETTING_FORCE_POLL, wantPoll)
|
|
365
|
+
task.defer(function()
|
|
366
|
+
Transport.stop()
|
|
367
|
+
task.wait(0.1)
|
|
368
|
+
connect()
|
|
369
|
+
end)
|
|
370
|
+
return {
|
|
371
|
+
mode = requested,
|
|
372
|
+
forcePoll = wantPoll,
|
|
373
|
+
changed = true,
|
|
374
|
+
note = "Reconnecting on the new transport; the next call will use it.",
|
|
375
|
+
}
|
|
376
|
+
end,
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
refreshMeta()
|
|
380
|
+
|
|
381
|
+
-- Connect on load unless the user explicitly disconnected last session.
|
|
382
|
+
if plugin:GetSetting(SETTING_AUTOCONNECT) ~= false then
|
|
383
|
+
connect()
|
|
384
|
+
else
|
|
385
|
+
Console.log("warn", "auto-connect disabled — press reconnect to start")
|
|
386
|
+
end
|