@el4cteo/rbx-studio-mcp 0.6.5 → 0.6.8
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 +4 -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 +290 -0
- package/dist/lib/opencloud.js.map +1 -0
- package/dist/tools/audio.js +96 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/data.js +133 -3
- package/dist/tools/data.js.map +1 -1
- package/dist/tools/exec.js +56 -1
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/scripts.js +124 -4
- 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 +182 -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 +361 -10
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Commands.luau +646 -622
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Phrase.luau +816 -766
- package/plugin/src/Prompt.luau +965 -961
- package/plugin/src/Secret.luau +86 -0
- package/plugin/src/Serialize.luau +759 -499
- package/plugin/src/handlers/Assets.luau +636 -587
- package/plugin/src/handlers/Audio.luau +411 -0
- package/plugin/src/handlers/Geometry.luau +722 -577
- package/plugin/src/handlers/Instances.luau +84 -4
- package/plugin/src/handlers/Spatial.luau +334 -0
- package/plugin/src/init.server.luau +883 -879
|
@@ -1,879 +1,883 @@
|
|
|
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
|
-
local Selection = game:GetService("Selection")
|
|
13
|
-
|
|
14
|
-
local Commands = require(script.Commands)
|
|
15
|
-
local Config = require(script.Config)
|
|
16
|
-
local Console = require(script.Console)
|
|
17
|
-
local Dispatch = require(script.Dispatch)
|
|
18
|
-
local History = require(script.History)
|
|
19
|
-
local LogBuffer = require(script.LogBuffer)
|
|
20
|
-
local Mirror = require(script.Mirror)
|
|
21
|
-
local Phrase = require(script.Phrase)
|
|
22
|
-
local ScriptEdit = require(script.ScriptEdit)
|
|
23
|
-
local Themes = require(script.Themes)
|
|
24
|
-
local Transport = require(script.Transport)
|
|
25
|
-
local Debug = require(script.handlers.Debug)
|
|
26
|
-
local Assets = require(script.handlers.Assets)
|
|
27
|
-
local Capture = require(script.handlers.Capture)
|
|
28
|
-
local Character = require(script.handlers.Character)
|
|
29
|
-
local Geometry = require(script.handlers.Geometry)
|
|
30
|
-
local Generate = require(script.handlers.Generate)
|
|
31
|
-
local Terrain = require(script.handlers.Terrain)
|
|
32
|
-
local World = require(script.handlers.World)
|
|
33
|
-
local
|
|
34
|
-
local
|
|
35
|
-
local
|
|
36
|
-
local
|
|
37
|
-
local
|
|
38
|
-
local
|
|
39
|
-
local
|
|
40
|
-
local
|
|
41
|
-
local
|
|
42
|
-
local
|
|
43
|
-
local
|
|
44
|
-
local
|
|
45
|
-
local
|
|
46
|
-
|
|
47
|
-
local
|
|
48
|
-
|
|
49
|
-
local
|
|
50
|
-
local
|
|
51
|
-
local
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
local
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
local
|
|
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
|
-
.. "session
|
|
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
|
-
|
|
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
|
-
Studio
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
end
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
the
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
Console.
|
|
444
|
-
end,
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if RunService:IsEdit() then
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if status ==
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
local
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
)
|
|
807
|
-
Console.
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
end)
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
Transport.
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
+
local Selection = game:GetService("Selection")
|
|
13
|
+
|
|
14
|
+
local Commands = require(script.Commands)
|
|
15
|
+
local Config = require(script.Config)
|
|
16
|
+
local Console = require(script.Console)
|
|
17
|
+
local Dispatch = require(script.Dispatch)
|
|
18
|
+
local History = require(script.History)
|
|
19
|
+
local LogBuffer = require(script.LogBuffer)
|
|
20
|
+
local Mirror = require(script.Mirror)
|
|
21
|
+
local Phrase = require(script.Phrase)
|
|
22
|
+
local ScriptEdit = require(script.ScriptEdit)
|
|
23
|
+
local Themes = require(script.Themes)
|
|
24
|
+
local Transport = require(script.Transport)
|
|
25
|
+
local Debug = require(script.handlers.Debug)
|
|
26
|
+
local Assets = require(script.handlers.Assets)
|
|
27
|
+
local Capture = require(script.handlers.Capture)
|
|
28
|
+
local Character = require(script.handlers.Character)
|
|
29
|
+
local Geometry = require(script.handlers.Geometry)
|
|
30
|
+
local Generate = require(script.handlers.Generate)
|
|
31
|
+
local Terrain = require(script.handlers.Terrain)
|
|
32
|
+
local World = require(script.handlers.World)
|
|
33
|
+
local Spatial = require(script.handlers.Spatial)
|
|
34
|
+
local Audio = require(script.handlers.Audio)
|
|
35
|
+
local Discover = require(script.handlers.Discover)
|
|
36
|
+
local Exec = require(script.handlers.Exec)
|
|
37
|
+
local Instances = require(script.handlers.Instances)
|
|
38
|
+
local Perf = require(script.handlers.Perf)
|
|
39
|
+
local Playtest = require(script.handlers.Playtest)
|
|
40
|
+
local Viewport = require(script.handlers.Viewport)
|
|
41
|
+
local Input = require(script.handlers.Input)
|
|
42
|
+
local Device = require(script.handlers.Device)
|
|
43
|
+
local Data = require(script.handlers.Data)
|
|
44
|
+
local Anim = require(script.handlers.Anim)
|
|
45
|
+
local Api = require(script.handlers.Api)
|
|
46
|
+
local Scripts = require(script.handlers.Scripts)
|
|
47
|
+
local Session = require(script.handlers.Session)
|
|
48
|
+
|
|
49
|
+
local SETTING_PORT = "port"
|
|
50
|
+
local SETTING_AUTOCONNECT = "autoConnect"
|
|
51
|
+
local SETTING_FORCE_POLL = "forcePoll"
|
|
52
|
+
local SETTING_THEME = "theme"
|
|
53
|
+
local SETTING_WIDGET_OPEN = "widgetOpen"
|
|
54
|
+
--[[
|
|
55
|
+
Whether the panel is allowed to come up on its own at all.
|
|
56
|
+
|
|
57
|
+
Separate from `widgetOpen`, which is where the user last left it. That one
|
|
58
|
+
answers "was it open?"; this one answers "may it open itself?", and they are
|
|
59
|
+
different questions -- someone who wants the panel there when they ask for it
|
|
60
|
+
and nowhere else has no way to say so with a single remembered flag, because
|
|
61
|
+
opening it once would arm it for every load afterwards.
|
|
62
|
+
|
|
63
|
+
Off means the toolbar button is the only thing that opens it: not a place
|
|
64
|
+
being opened, not a Studio launch, not pressing Play. Set by `autoopen off`.
|
|
65
|
+
]]
|
|
66
|
+
local SETTING_AUTO_OPEN = "autoOpen"
|
|
67
|
+
local SETTING_WIDGET_SIZE = "widgetSize"
|
|
68
|
+
local SETTING_WIDGET_TRACE = "widgetTrace"
|
|
69
|
+
|
|
70
|
+
-- Enough to cover a close and the launch after it, without growing forever.
|
|
71
|
+
local TRACE_LIMIT = 24
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
--[[
|
|
75
|
+
A fresh id per plugin load, which in practice means one per Studio window.
|
|
76
|
+
|
|
77
|
+
This was originally persisted with `plugin:SetSetting`, on the reasoning that a
|
|
78
|
+
stable id keeps reconnects mapping to the same session. That is wrong as soon
|
|
79
|
+
as the user opens a second window: plugin settings live in one file shared by
|
|
80
|
+
every Studio process, so both windows announce the same id, and the server
|
|
81
|
+
treats the second connection as the first one reconnecting -- closing the
|
|
82
|
+
original stream and making it impossible to address the two places separately.
|
|
83
|
+
|
|
84
|
+
Held in memory instead. A reconnect within one load (the SSE stream hits its
|
|
85
|
+
30-minute cap) reuses this id, and `plugin.Unloading` detaches cleanly on
|
|
86
|
+
reload, so ghost entries do not accumulate.
|
|
87
|
+
]]
|
|
88
|
+
local SESSION_ID = HttpService:GenerateGUID(false)
|
|
89
|
+
|
|
90
|
+
local function studioId(): string
|
|
91
|
+
return SESSION_ID
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
--[[
|
|
95
|
+
Whether this copy of the plugin can reach the bridge at all.
|
|
96
|
+
|
|
97
|
+
Pressing Play loads the plugin into the playtest's DataModels as well as the
|
|
98
|
+
editor's, and HttpService refuses every request from a client one: "Http
|
|
99
|
+
requests can only be executed by game server". The transport read that as a
|
|
100
|
+
dropped connection and retried forever, filling the console with red while
|
|
101
|
+
nothing was actually wrong.
|
|
102
|
+
|
|
103
|
+
The editor session and the play session's server can both connect and are
|
|
104
|
+
worth connecting -- addressing a running server is useful. The client half
|
|
105
|
+
simply says so once and stops.
|
|
106
|
+
]]
|
|
107
|
+
local function canConnect(): (boolean, string?)
|
|
108
|
+
if RunService:IsEdit() or RunService:IsServer() then
|
|
109
|
+
return true, nil
|
|
110
|
+
end
|
|
111
|
+
--[[
|
|
112
|
+
The wording names the fix, because the symptom is indistinguishable
|
|
113
|
+
from a broken server: a user watching this window during a playtest
|
|
114
|
+
sees a console that logs nothing while tools plainly work, and has no
|
|
115
|
+
way to guess that the activity is in a different view of the same
|
|
116
|
+
Studio. Reported once here and again as the strip's caption, since
|
|
117
|
+
one line scrolled off the top is easy to miss.
|
|
118
|
+
]]
|
|
119
|
+
return false,
|
|
120
|
+
"client view of a playtest -- Studio forbids client sessions from making HTTP "
|
|
121
|
+
.. "requests, so this panel cannot reach the bridge itself. It is MIRRORING "
|
|
122
|
+
.. "the playtest's server session instead, so the log and the strip below "
|
|
123
|
+
.. "are live. Switch to the Server view (Test tab, Current: Server) for the "
|
|
124
|
+
.. "session that is actually connected."
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
-- First thing, before any handler or the transport can log: the buffer only
|
|
128
|
+
-- holds what was printed after it subscribed, so every line ahead of this call
|
|
129
|
+
-- is unrecoverable. This runs even in a client session that will never connect,
|
|
130
|
+
-- since the console tool reads it and connectivity is a separate question.
|
|
131
|
+
LogBuffer.start()
|
|
132
|
+
|
|
133
|
+
local storedPort = plugin:GetSetting(SETTING_PORT)
|
|
134
|
+
if typeof(storedPort) == "number" then
|
|
135
|
+
Config.setPort(storedPort)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Transport.setForcePoll(plugin:GetSetting(SETTING_FORCE_POLL) == true)
|
|
139
|
+
|
|
140
|
+
--[[
|
|
141
|
+
The console's colour preset, restored before anything is drawn.
|
|
142
|
+
|
|
143
|
+
Applied here rather than after mounting so the panel is built in the right
|
|
144
|
+
palette from the start -- restoring it afterwards would flash the default
|
|
145
|
+
theme for a frame on every Studio launch. An unknown id (a preset renamed,
|
|
146
|
+
or a setting written by a newer build) falls back to the default rather than
|
|
147
|
+
failing, which is `Themes.get`'s job.
|
|
148
|
+
]]
|
|
149
|
+
local storedTheme = plugin:GetSetting(SETTING_THEME)
|
|
150
|
+
if typeof(storedTheme) == "string" then
|
|
151
|
+
Themes.use(storedTheme)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
--[[
|
|
155
|
+
Whether the restore above still has to be pushed into the console.
|
|
156
|
+
|
|
157
|
+
`Themes.use` moves the ACTIVE ID, and anything that reads the palette live
|
|
158
|
+
picks the change up for free -- which is why the prism cell came back on the
|
|
159
|
+
saved preset. `Console` does not read it live: it caches the palette in an
|
|
160
|
+
upvalue at module load, deliberately, so that forty read sites stay plain
|
|
161
|
+
field accesses. Module load happens at the `require` above, which is BEFORE
|
|
162
|
+
this line, so the cache held the default while the id said otherwise, and a
|
|
163
|
+
reload came back as the saved prism drawn in the default's colours.
|
|
164
|
+
|
|
165
|
+
It cannot simply be applied here either -- the console is not mounted yet.
|
|
166
|
+
So it is remembered and applied the moment it can be, right after mounting.
|
|
167
|
+
]]
|
|
168
|
+
local themeNeedsApplying = typeof(storedTheme) == "string" and Themes.activeId() == storedTheme
|
|
169
|
+
|
|
170
|
+
Session.register()
|
|
171
|
+
Discover.register()
|
|
172
|
+
Debug.register()
|
|
173
|
+
Instances.register()
|
|
174
|
+
Perf.register(plugin)
|
|
175
|
+
Playtest.register()
|
|
176
|
+
Capture.register()
|
|
177
|
+
Assets.register()
|
|
178
|
+
Character.register()
|
|
179
|
+
Geometry.register()
|
|
180
|
+
Generate.register()
|
|
181
|
+
Terrain.register()
|
|
182
|
+
World.register()
|
|
183
|
+
Spatial.register()
|
|
184
|
+
Audio.register()
|
|
185
|
+
Exec.register()
|
|
186
|
+
Viewport.register()
|
|
187
|
+
Scripts.register()
|
|
188
|
+
Input.register()
|
|
189
|
+
Device.register()
|
|
190
|
+
Data.register()
|
|
191
|
+
Anim.register()
|
|
192
|
+
Api.register()
|
|
193
|
+
|
|
194
|
+
--[[
|
|
195
|
+
The toolbar button.
|
|
196
|
+
|
|
197
|
+
The icon has to be an uploaded asset: `CreateButton` takes a content string,
|
|
198
|
+
and the only ones Studio resolves are `rbxassetid://` for uploaded images and
|
|
199
|
+
`rbxasset://` for files that ship inside Studio itself. A path to something in
|
|
200
|
+
this repository is not one of them, which is why the mark in `assets/` has to
|
|
201
|
+
go through an upload before it can appear here.
|
|
202
|
+
|
|
203
|
+
It used to borrow `textures/ui/common/robux.png` -- a Robux coin, sitting in
|
|
204
|
+
the toolbar next to a plugin that has nothing to do with purchases.
|
|
205
|
+
]]
|
|
206
|
+
local toolbar = plugin:CreateToolbar("rbx-studio")
|
|
207
|
+
local button = toolbar:CreateButton(
|
|
208
|
+
"rbx-studio",
|
|
209
|
+
"Show the rbx-studio console",
|
|
210
|
+
"rbxassetid://125390773465346"
|
|
211
|
+
)
|
|
212
|
+
button.ClickableWhenViewportHidden = true
|
|
213
|
+
|
|
214
|
+
--[[
|
|
215
|
+
The panel, opened and sized the way the user last left it -- in EVERY
|
|
216
|
+
DataModel, which is the whole point.
|
|
217
|
+
|
|
218
|
+
Pressing Play loads this plugin again into the playtest's DataModels, and a
|
|
219
|
+
dock widget belongs to the DataModel that created it: the editor's is hidden
|
|
220
|
+
along with the editor's view, and the playtest's is a brand new widget that
|
|
221
|
+
Studio brings up closed and at the default size. So the panel vanished on
|
|
222
|
+
every playtest and came back, when the user re-opened it from the toolbar,
|
|
223
|
+
as a 560x320 float with whatever size they had chosen thrown away.
|
|
224
|
+
|
|
225
|
+
Studio's own restore does not cross that boundary, so the preference is kept
|
|
226
|
+
here instead and passed in as the INITIAL state, with `overrideEnabledRestore`
|
|
227
|
+
set so it is honoured rather than second-guessed. Written only from the
|
|
228
|
+
editor session -- see below -- so a playtest starting or ending can never
|
|
229
|
+
record a decision the user did not make.
|
|
230
|
+
]]
|
|
231
|
+
local storedOpen = plugin:GetSetting(SETTING_WIDGET_OPEN)
|
|
232
|
+
local wantOpen = if typeof(storedOpen) == "boolean" then storedOpen else true
|
|
233
|
+
|
|
234
|
+
--[[
|
|
235
|
+
`autoopen off` means this load does not put the panel on screen.
|
|
236
|
+
|
|
237
|
+
Every DataModel, deliberately. The first version of this only covered the
|
|
238
|
+
playtest, on the reading that a window over the game view was the complaint
|
|
239
|
+
-- but a panel that reappears every time a place is opened is the same
|
|
240
|
+
complaint arriving through the other door, and one switch for "open yourself"
|
|
241
|
+
is easier to hold in your head than two.
|
|
242
|
+
|
|
243
|
+
Read here rather than acted on later because a dock widget's initial state is
|
|
244
|
+
fixed when it is created: bringing it up and hiding it again would flash a
|
|
245
|
+
window on screen, which is the thing being asked for less of. `widgetOpen` is
|
|
246
|
+
left exactly as it was, so turning this back on restores the panel to however
|
|
247
|
+
it was last left rather than to a default.
|
|
248
|
+
]]
|
|
249
|
+
local storedAutoOpen = plugin:GetSetting(SETTING_AUTO_OPEN)
|
|
250
|
+
local autoOpen = if typeof(storedAutoOpen) == "boolean" then storedAutoOpen else true
|
|
251
|
+
if not autoOpen then
|
|
252
|
+
wantOpen = false
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
local floatWidth, floatHeight = 560, 320
|
|
256
|
+
local storedSize = plugin:GetSetting(SETTING_WIDGET_SIZE)
|
|
257
|
+
if typeof(storedSize) == "table" then
|
|
258
|
+
local saved = storedSize :: { [string]: any }
|
|
259
|
+
local x, y = tonumber(saved.x), tonumber(saved.y)
|
|
260
|
+
-- Guarded against nonsense: a zero or absurd size saved from a docked or
|
|
261
|
+
-- mid-teardown widget would otherwise be unrecoverable without clearing
|
|
262
|
+
-- settings by hand.
|
|
263
|
+
if x ~= nil and y ~= nil and x >= 360 and y >= 200 and x <= 4000 and y <= 4000 then
|
|
264
|
+
floatWidth, floatHeight = math.floor(x), math.floor(y)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
local widget = plugin:CreateDockWidgetPluginGuiAsync(
|
|
269
|
+
"StudioMCP_Console",
|
|
270
|
+
DockWidgetPluginGuiInfo.new(
|
|
271
|
+
Enum.InitialDockState.Float,
|
|
272
|
+
wantOpen,
|
|
273
|
+
-- Override Studio's own enabled-restore: ours is the one that survives
|
|
274
|
+
-- the hop into a playtest DataModel, and two restores disagreeing is
|
|
275
|
+
-- what produced a panel that was open in the editor and closed in play.
|
|
276
|
+
true,
|
|
277
|
+
floatWidth,
|
|
278
|
+
floatHeight,
|
|
279
|
+
360,
|
|
280
|
+
200
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
widget.Title = "rbx-studio"
|
|
284
|
+
|
|
285
|
+
--[[
|
|
286
|
+
Remember what the user does with the panel, from the editor only.
|
|
287
|
+
|
|
288
|
+
The editor session is the one whose Enabled and size changes are actually
|
|
289
|
+
the user's: a playtest DataModel's widget is created, shown and destroyed by
|
|
290
|
+
Studio around the test, and letting those transitions write would persist a
|
|
291
|
+
"closed" the user never asked for -- reintroducing the bug through the back
|
|
292
|
+
door.
|
|
293
|
+
]]
|
|
294
|
+
--[[
|
|
295
|
+
Whether the panel is being closed BY THE USER, as opposed to by Studio.
|
|
296
|
+
|
|
297
|
+
Set only for the moment the toolbar button toggles the widget, which is the
|
|
298
|
+
one close this plugin can attribute with certainty.
|
|
299
|
+
]]
|
|
300
|
+
local closingByHand = false
|
|
301
|
+
|
|
302
|
+
--[[
|
|
303
|
+
A breadcrumb trail across sessions, so this stops being guesswork.
|
|
304
|
+
|
|
305
|
+
Two fixes for "the panel does not open any more" were shipped on inference
|
|
306
|
+
rather than observation, and this is what finally settled it. Closing a place
|
|
307
|
+
traces as:
|
|
308
|
+
|
|
309
|
+
load open=true / enabled=false / plugin.Unloading / widget.Destroying
|
|
310
|
+
|
|
311
|
+
all within the same second. So both teardown signals DO fire, and promptly --
|
|
312
|
+
the opposite of what the second fix was written to work around.
|
|
313
|
+
|
|
314
|
+
The reason that fix looked like it failed is duller and worth remembering: a
|
|
315
|
+
`false` written by an older build is sticky. Nothing writes `true` again
|
|
316
|
+
until the panel is opened by hand, so a stored close outlives the bug that
|
|
317
|
+
stored it and makes the next fix look broken too.
|
|
318
|
+
|
|
319
|
+
The trail is kept because this bug has now been misdiagnosed twice, and
|
|
320
|
+
twenty-four rows is a cheap price for never having to guess at it again. It
|
|
321
|
+
also shows the X could be honoured through Unloading -- deliberately not
|
|
322
|
+
done, since attribution cannot regress and inference has twice.
|
|
323
|
+
]]
|
|
324
|
+
local function trace(what: string)
|
|
325
|
+
local ok, existing = pcall(function()
|
|
326
|
+
return plugin:GetSetting(SETTING_WIDGET_TRACE)
|
|
327
|
+
end)
|
|
328
|
+
local rows: { string } = if ok and typeof(existing) == "table" then existing :: { string } else {}
|
|
329
|
+
table.insert(rows, string.format("%d %s", os.time(), what))
|
|
330
|
+
while #rows > TRACE_LIMIT do
|
|
331
|
+
table.remove(rows, 1)
|
|
332
|
+
end
|
|
333
|
+
pcall(function()
|
|
334
|
+
plugin:SetSetting(SETTING_WIDGET_TRACE, rows)
|
|
335
|
+
end)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
if RunService:IsEdit() then
|
|
339
|
+
trace(string.format("load open=%s", tostring(storedOpen)))
|
|
340
|
+
|
|
341
|
+
plugin.Unloading:Connect(function()
|
|
342
|
+
trace("plugin.Unloading")
|
|
343
|
+
end)
|
|
344
|
+
widget.Destroying:Connect(function()
|
|
345
|
+
trace("widget.Destroying")
|
|
346
|
+
end)
|
|
347
|
+
|
|
348
|
+
--[[
|
|
349
|
+
Only a close this plugin performed is remembered.
|
|
350
|
+
|
|
351
|
+
Opening is recorded at once -- nothing but a user opens a panel, so a
|
|
352
|
+
`true` is always genuine. A `false` is the dangerous one, because Studio
|
|
353
|
+
disables the widget while tearing a place down and that is
|
|
354
|
+
indistinguishable, from in here, from the user clicking the X. Guessing
|
|
355
|
+
wrong writes a close nobody performed, and once `false` is stored the
|
|
356
|
+
panel never opens again until it is opened by hand -- which is exactly
|
|
357
|
+
the bug people report, twice now after a "fix".
|
|
358
|
+
|
|
359
|
+
So the guessing is over. The toolbar button is the one close this code
|
|
360
|
+
performs itself, and it is the only one written down. Closing the panel
|
|
361
|
+
with the X is not persisted, so it will be back on the next launch.
|
|
362
|
+
That is a deliberate trade: a panel that reappears when you did not want
|
|
363
|
+
it is a small annoyance, and a panel that never appears again reads as a
|
|
364
|
+
broken plugin.
|
|
365
|
+
|
|
366
|
+
The trace above is what would let the X be honoured too -- if it shows a
|
|
367
|
+
teardown signal that reliably beats the disable, this can use it.
|
|
368
|
+
]]
|
|
369
|
+
widget:GetPropertyChangedSignal("Enabled"):Connect(function()
|
|
370
|
+
if widget.Enabled then
|
|
371
|
+
trace("enabled=true -> save true")
|
|
372
|
+
plugin:SetSetting(SETTING_WIDGET_OPEN, true)
|
|
373
|
+
return
|
|
374
|
+
end
|
|
375
|
+
if closingByHand then
|
|
376
|
+
trace("enabled=false byHand -> save false")
|
|
377
|
+
plugin:SetSetting(SETTING_WIDGET_OPEN, false)
|
|
378
|
+
return
|
|
379
|
+
end
|
|
380
|
+
-- Studio's doing, or the X. Either way, not written down.
|
|
381
|
+
trace("enabled=false ignored")
|
|
382
|
+
end)
|
|
383
|
+
|
|
384
|
+
-- Only a real, sane size, and only while the panel is up: a hidden or
|
|
385
|
+
-- collapsing widget reports sizes that are not a choice.
|
|
386
|
+
local function persistSize(): boolean
|
|
387
|
+
local size = widget.AbsoluteSize
|
|
388
|
+
if unloading or not widget.Enabled or size.X < 360 or size.Y < 200 then
|
|
389
|
+
return false
|
|
390
|
+
end
|
|
391
|
+
plugin:SetSetting(SETTING_WIDGET_SIZE, { x = math.floor(size.X), y = math.floor(size.Y) })
|
|
392
|
+
return true
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
widget:GetPropertyChangedSignal("AbsoluteSize"):Connect(persistSize)
|
|
396
|
+
|
|
397
|
+
--[[
|
|
398
|
+
Seed from the size Studio has already restored, rather than waiting for
|
|
399
|
+
a resize that may never come.
|
|
400
|
+
|
|
401
|
+
Without this the setting stays empty until the user happens to drag the
|
|
402
|
+
panel's edge, so the first playtest inherits the 560x320 default and the
|
|
403
|
+
window visibly shrinks -- which is the same complaint as it vanishing,
|
|
404
|
+
one step later. Studio settles the geometry a few frames after the widget
|
|
405
|
+
is created and fires no change event for it, so it is polled briefly and
|
|
406
|
+
then left alone.
|
|
407
|
+
]]
|
|
408
|
+
task.defer(function()
|
|
409
|
+
for _ = 1, 40 do
|
|
410
|
+
if persistSize() then
|
|
411
|
+
return
|
|
412
|
+
end
|
|
413
|
+
task.wait(0.1)
|
|
414
|
+
end
|
|
415
|
+
end)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
local currentStatus: Transport.Status = "disconnected"
|
|
419
|
+
|
|
420
|
+
local function refreshMeta()
|
|
421
|
+
Console.setStatus(
|
|
422
|
+
currentStatus,
|
|
423
|
+
string.format(
|
|
424
|
+
"127.0.0.1:%d %s build %s",
|
|
425
|
+
Config.getPort(),
|
|
426
|
+
Transport.getMode(),
|
|
427
|
+
Config.BUILD_ID
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
button:SetActive(currentStatus == "connected")
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
local connect: () -> ()
|
|
434
|
+
|
|
435
|
+
Console.mount(widget, {
|
|
436
|
+
onReconnect = function()
|
|
437
|
+
Console.log("info", "reconnect requested")
|
|
438
|
+
Transport.stop()
|
|
439
|
+
task.wait(0.2)
|
|
440
|
+
connect()
|
|
441
|
+
end,
|
|
442
|
+
onClear = function()
|
|
443
|
+
Console.clear()
|
|
444
|
+
end,
|
|
445
|
+
onTheme = function(id)
|
|
446
|
+
plugin:SetSetting(SETTING_THEME, id)
|
|
447
|
+
Console.log("dim", string.format("theme: %s", id))
|
|
448
|
+
end,
|
|
449
|
+
onSubmit = function(line)
|
|
450
|
+
Commands.run(line)
|
|
451
|
+
end,
|
|
452
|
+
onComplete = function(prefix)
|
|
453
|
+
return Commands.complete(prefix)
|
|
454
|
+
end,
|
|
455
|
+
onSuggest = function(prefix)
|
|
456
|
+
return Commands.suggest(prefix)
|
|
457
|
+
end,
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
--[[
|
|
461
|
+
What the command line cannot reach on its own.
|
|
462
|
+
|
|
463
|
+
`plugin:SetSetting` is unavailable inside a ModuleScript, and the transport's
|
|
464
|
+
lifecycle lives here rather than in the console, so the handful of commands
|
|
465
|
+
that need either are handed a function instead of a dependency.
|
|
466
|
+
]]
|
|
467
|
+
Commands.setup({
|
|
468
|
+
reconnect = function()
|
|
469
|
+
Console.log("info", "reconnect requested")
|
|
470
|
+
Transport.stop()
|
|
471
|
+
task.wait(0.2)
|
|
472
|
+
connect()
|
|
473
|
+
end,
|
|
474
|
+
savePort = function(value)
|
|
475
|
+
plugin:SetSetting(SETTING_PORT, value)
|
|
476
|
+
end,
|
|
477
|
+
saveTheme = function(id)
|
|
478
|
+
plugin:SetSetting(SETTING_THEME, id)
|
|
479
|
+
end,
|
|
480
|
+
autoOpen = function()
|
|
481
|
+
return autoOpen
|
|
482
|
+
end,
|
|
483
|
+
saveAutoOpen = function(value)
|
|
484
|
+
-- The live copy moves too, so `autoopen` with no argument reports what
|
|
485
|
+
-- was just set rather than what the session started with.
|
|
486
|
+
autoOpen = value
|
|
487
|
+
plugin:SetSetting(SETTING_AUTO_OPEN, value)
|
|
488
|
+
end,
|
|
489
|
+
status = function()
|
|
490
|
+
return currentStatus
|
|
491
|
+
end,
|
|
492
|
+
studioId = studioId,
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
-- The saved preset, now that there is something to paint. Done immediately
|
|
496
|
+
-- after mounting and before the first line is logged, so nothing is ever drawn
|
|
497
|
+
-- in the wrong palette.
|
|
498
|
+
if themeNeedsApplying then
|
|
499
|
+
Console.applyTheme()
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
--[[
|
|
503
|
+
Keeps the prompt row showing what Studio has selected.
|
|
504
|
+
|
|
505
|
+
`Selection.SelectionChanged` is the event the panel was missing. Everything
|
|
506
|
+
about the selection was previously available only on demand, through
|
|
507
|
+
`studio_status`, which answers an agent's question but not the user's: they
|
|
508
|
+
are the one typing "make this bigger", and the word is only safe to type if
|
|
509
|
+
they can see what it means without leaving the panel.
|
|
510
|
+
|
|
511
|
+
Debounced through a generation counter rather than a timer handle, the same
|
|
512
|
+
way `recordCall` schedules its summary. Drag-selecting a group in the
|
|
513
|
+
viewport fires this once per instance added -- dozens of times in a second
|
|
514
|
+
-- and re-rendering a label on every one of them is work nobody sees.
|
|
515
|
+
]]
|
|
516
|
+
local selectionGeneration = 0
|
|
517
|
+
|
|
518
|
+
local function describeSelection(): string
|
|
519
|
+
local picked = Selection:Get()
|
|
520
|
+
if #picked == 0 then
|
|
521
|
+
return ""
|
|
522
|
+
end
|
|
523
|
+
if #picked == 1 then
|
|
524
|
+
-- The name alone. The class is in the Properties panel and the path is
|
|
525
|
+
-- in the Explorer; what this line is for is confirming WHICH thing.
|
|
526
|
+
return string.format("\u{25A3} %s", picked[1].Name)
|
|
527
|
+
end
|
|
528
|
+
return string.format("\u{25A3} %d selected", #picked)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
local function watchSelection()
|
|
532
|
+
Selection.SelectionChanged:Connect(function()
|
|
533
|
+
selectionGeneration += 1
|
|
534
|
+
local mine = selectionGeneration
|
|
535
|
+
task.delay(0.12, function()
|
|
536
|
+
if selectionGeneration ~= mine then
|
|
537
|
+
return
|
|
538
|
+
end
|
|
539
|
+
-- Guarded: an instance can be destroyed between the event and this
|
|
540
|
+
-- running, and reading `.Name` on it throws.
|
|
541
|
+
local ok, text = pcall(describeSelection)
|
|
542
|
+
Console.setSelection(if ok then text else "")
|
|
543
|
+
end)
|
|
544
|
+
end)
|
|
545
|
+
Console.setSelection(describeSelection())
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
-- Only in the editor. A playtest session has its own Selection service that
|
|
549
|
+
-- nobody is clicking in, and the client view cannot reach the panel at all.
|
|
550
|
+
if RunService:IsEdit() then
|
|
551
|
+
watchSelection()
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
--[[
|
|
555
|
+
The log, picked up where the last copy of this plugin left it.
|
|
556
|
+
|
|
557
|
+
Restored before the banner below rather than after it, so the carried rows
|
|
558
|
+
read as what they are -- what was on screen a moment ago -- with this
|
|
559
|
+
session's first line underneath them instead of buried in the middle.
|
|
560
|
+
|
|
561
|
+
Every session reads; only the editor writes. See `History` for why.
|
|
562
|
+
]]
|
|
563
|
+
History.attach(plugin, Console.snapshot)
|
|
564
|
+
local carried = History.restore()
|
|
565
|
+
if #carried > 0 then
|
|
566
|
+
Console.restore(carried)
|
|
567
|
+
Console.log(
|
|
568
|
+
"dim",
|
|
569
|
+
string.format("carried %d line%s over", #carried, if #carried == 1 then "" else "s"),
|
|
570
|
+
if RunService:IsEdit() then "from the last session" else "from the editor"
|
|
571
|
+
)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
if RunService:IsEdit() then
|
|
575
|
+
History.start()
|
|
576
|
+
plugin.Unloading:Connect(function()
|
|
577
|
+
History.stop()
|
|
578
|
+
-- The loop cannot save what happens after it has stopped, and closing
|
|
579
|
+
-- Studio is exactly when the last few rows matter most.
|
|
580
|
+
History.flush()
|
|
581
|
+
end)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
--[[
|
|
585
|
+
The playtest halves, joined.
|
|
586
|
+
|
|
587
|
+
The server half can reach the bridge and sees every command; the client half
|
|
588
|
+
is the one Studio actually shows during a playtest and can reach nothing. So
|
|
589
|
+
the server relays its console events and the client replays them, which is
|
|
590
|
+
the only way the panel in front of the user reacts to the work being done.
|
|
591
|
+
|
|
592
|
+
Note which side sets the observer: only the SERVER. The client replays
|
|
593
|
+
events through the same `Console` functions, and if it were also observing
|
|
594
|
+
it would echo each one straight back into the channel.
|
|
595
|
+
]]
|
|
596
|
+
if RunService:IsEdit() then
|
|
597
|
+
-- The only observer the edit session needs: every logged row marks the
|
|
598
|
+
-- stored copy stale, and the save loop above decides when to write it.
|
|
599
|
+
Console.setObserver(function()
|
|
600
|
+
History.touch()
|
|
601
|
+
end)
|
|
602
|
+
elseif Mirror.isPlaytestServer() then
|
|
603
|
+
Mirror.startServer()
|
|
604
|
+
Console.setObserver(function(kind, arguments)
|
|
605
|
+
Mirror.send(kind, arguments)
|
|
606
|
+
end)
|
|
607
|
+
-- The channel itself stays shut until `onStatus` reports the connection;
|
|
608
|
+
-- see `Mirror.open`.
|
|
609
|
+
elseif Mirror.isPlaytestClient() then
|
|
610
|
+
Console.setMirroring(true)
|
|
611
|
+
Mirror.startClient(function(kind, arguments)
|
|
612
|
+
if kind == "log" then
|
|
613
|
+
local level, message, detail = arguments[1], arguments[2], arguments[3]
|
|
614
|
+
if typeof(level) == "string" and typeof(message) == "string" then
|
|
615
|
+
Console.log(
|
|
616
|
+
level :: any,
|
|
617
|
+
message,
|
|
618
|
+
if typeof(detail) == "string" then detail else nil
|
|
619
|
+
)
|
|
620
|
+
end
|
|
621
|
+
elseif kind == "beginCall" then
|
|
622
|
+
local title, callKind = arguments[1], arguments[2]
|
|
623
|
+
if typeof(title) == "string" and typeof(callKind) == "string" then
|
|
624
|
+
Console.beginCall(title, callKind)
|
|
625
|
+
end
|
|
626
|
+
elseif kind == "recordCall" then
|
|
627
|
+
local ok, milliseconds = arguments[1], arguments[2]
|
|
628
|
+
if typeof(ok) == "boolean" and typeof(milliseconds) == "number" then
|
|
629
|
+
Console.recordCall(ok, milliseconds)
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
end)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
Console.log("info", string.format("rbx-studio v%s", Config.PLUGIN_VERSION), "build " .. Config.BUILD_ID)
|
|
636
|
+
Console.log("dim", string.format("place: %s (%d)", game.Name, game.PlaceId))
|
|
637
|
+
|
|
638
|
+
local function onStatus(status: Transport.Status, detail: string?)
|
|
639
|
+
local previous = currentStatus
|
|
640
|
+
currentStatus = status
|
|
641
|
+
refreshMeta()
|
|
642
|
+
|
|
643
|
+
-- Only narrate transitions. The transport re-reports its state on every
|
|
644
|
+
-- reconnect attempt, and echoing an unchanged status would bury real events.
|
|
645
|
+
if status == previous and detail == nil then
|
|
646
|
+
return
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
if status == "connected" then
|
|
650
|
+
Console.log(
|
|
651
|
+
"ok",
|
|
652
|
+
string.format("connected to 127.0.0.1:%d", Config.getPort()),
|
|
653
|
+
if detail then "(" .. detail .. ")" else nil
|
|
654
|
+
)
|
|
655
|
+
-- After the row, not before it: this session's own arrival is not news
|
|
656
|
+
-- to the client view, which announced its own a moment ago.
|
|
657
|
+
Mirror.open()
|
|
658
|
+
elseif status == "connecting" then
|
|
659
|
+
Console.log("dim", string.format("connecting to 127.0.0.1:%d...", Config.getPort()))
|
|
660
|
+
else
|
|
661
|
+
Console.log("error", "disconnected", detail)
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
--[[
|
|
666
|
+
Runs each command on its own task. Handlers may yield -- `UpdateSourceAsync`
|
|
667
|
+
and any `*Async` call does -- and serialising them would let one slow edit
|
|
668
|
+
stall every other request on the stream.
|
|
669
|
+
]]
|
|
670
|
+
local function onCommand(id: string, op: string, params: { [string]: any }?)
|
|
671
|
+
task.spawn(function()
|
|
672
|
+
local startedAt = os.clock()
|
|
673
|
+
--[[
|
|
674
|
+
Named for what it does, not for how it travels. `script.edit` on
|
|
675
|
+
ServerScriptService.Systems.KillBrick reads as "Edit KillBrick",
|
|
676
|
+
which is the thing someone watching actually wants to know; the wire
|
|
677
|
+
name is kept alongside so the log still maps onto the protocol when
|
|
678
|
+
something needs debugging.
|
|
679
|
+
]]
|
|
680
|
+
local title = Phrase.of(op, params)
|
|
681
|
+
--[[
|
|
682
|
+
Announced synchronously, unlike the logging below.
|
|
683
|
+
|
|
684
|
+
Deferring this looked like free latency and was not. `beginCall` sets
|
|
685
|
+
two labels and some fields -- it never touches the RichText log, which
|
|
686
|
+
is where the cost actually is -- and deferring it let a call that
|
|
687
|
+
finished inside one frame record its result before the call had been
|
|
688
|
+
announced, so the bar on the trace lost the name it was supposed to
|
|
689
|
+
carry. A microsecond is not worth an ordering hazard.
|
|
690
|
+
]]
|
|
691
|
+
Console.beginCall(title, Phrase.kindOf(op))
|
|
692
|
+
|
|
693
|
+
local result = Dispatch.invoke(id, op, params)
|
|
694
|
+
local milliseconds = (os.clock() - startedAt) * 1000
|
|
695
|
+
|
|
696
|
+
--[[
|
|
697
|
+
The answer goes out before the console hears about it.
|
|
698
|
+
|
|
699
|
+
This used to be the last line of the function, which put a
|
|
700
|
+
`table.concat` of three hundred strings, a RichText relayout of the
|
|
701
|
+
whole log, two `Instance.new` calls and a forty-bar relayout in front
|
|
702
|
+
of the reply on its way back to the agent. None of that is work the
|
|
703
|
+
caller asked for, and all of it was being billed to the round trip
|
|
704
|
+
this project measures. Drawing happens below, on time the agent is no
|
|
705
|
+
longer waiting for.
|
|
706
|
+
]]
|
|
707
|
+
Transport.sendResult(result)
|
|
708
|
+
|
|
709
|
+
local elapsed = Console.durationText(milliseconds)
|
|
710
|
+
Console.recordCall(result.ok, milliseconds)
|
|
711
|
+
|
|
712
|
+
if result.ok then
|
|
713
|
+
Console.log("reply", title, elapsed)
|
|
714
|
+
else
|
|
715
|
+
local err = result.error
|
|
716
|
+
Console.log(
|
|
717
|
+
"error",
|
|
718
|
+
string.format("%s failed: %s", title, if err then err.code else "unknown"),
|
|
719
|
+
elapsed
|
|
720
|
+
)
|
|
721
|
+
if err and err.message then
|
|
722
|
+
Console.log("dim", " " .. err.message)
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
end)
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
--[[
|
|
729
|
+
Where a peer's call ran, in the width the detail column has.
|
|
730
|
+
|
|
731
|
+
The latency already lives there and only about fourteen characters fit
|
|
732
|
+
beside a message, so this is two letters and a space rather than "playtest
|
|
733
|
+
server". Which half of a playtest it was does not matter to someone reading
|
|
734
|
+
the editor's log afterwards; that it was not the editor does.
|
|
735
|
+
]]
|
|
736
|
+
local function peerTag(from: any): string
|
|
737
|
+
if typeof(from) ~= "string" then
|
|
738
|
+
return "peer"
|
|
739
|
+
end
|
|
740
|
+
return if from == "edit" then "edit" else "play"
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
--[[
|
|
744
|
+
Bridge news that is not a command.
|
|
745
|
+
|
|
746
|
+
Kept deliberately narrow: an unknown event is ignored rather than logged,
|
|
747
|
+
because a newer server talking to an older plugin is a supported situation
|
|
748
|
+
and "unknown event" rows would be the only symptom of it working correctly.
|
|
749
|
+
]]
|
|
750
|
+
local function onEvent(event: { [string]: any })
|
|
751
|
+
if event.event == "clients" and typeof(event.count) == "number" then
|
|
752
|
+
Console.setClients(event.count, if typeof(event.list) == "table" then event.list else nil)
|
|
753
|
+
elseif event.event == "agent" and event.state == "finished" then
|
|
754
|
+
Console.agentFinished()
|
|
755
|
+
elseif event.event == "console" and typeof(event.message) == "string" then
|
|
756
|
+
--[[
|
|
757
|
+
A line from an agent the panel itself started.
|
|
758
|
+
|
|
759
|
+
Pushed rather than returned because a run lasts minutes: the request
|
|
760
|
+
that started it answered immediately with "started", and everything
|
|
761
|
+
after that has to arrive on the stream the plugin already holds open.
|
|
762
|
+
]]
|
|
763
|
+
Console.log(
|
|
764
|
+
(if typeof(event.level) == "string" then event.level else "dim") :: any,
|
|
765
|
+
event.message,
|
|
766
|
+
if typeof(event.detail) == "string" then event.detail else nil
|
|
767
|
+
)
|
|
768
|
+
elseif event.event == "agent-run" then
|
|
769
|
+
Console.setPromptBusy(event.state == "running")
|
|
770
|
+
elseif event.event == "peer" and typeof(event.op) == "string" then
|
|
771
|
+
--[[
|
|
772
|
+
A command another session on this place just finished.
|
|
773
|
+
|
|
774
|
+
Logged as if it had run here, because as far as the user is concerned
|
|
775
|
+
it did: they pressed Play, watched the agent work, pressed Stop, and
|
|
776
|
+
the panel that comes back is this one. Without this it comes back with
|
|
777
|
+
a hole in it exactly the length of the playtest.
|
|
778
|
+
|
|
779
|
+
The prism is bumped too -- `beginCall` then `recordCall`, back to
|
|
780
|
+
back -- so the trace carries a bar for the call and the session
|
|
781
|
+
counters count it. Nothing here reaches the transport, so a peer row
|
|
782
|
+
can never be announced onwards and two sessions cannot echo forever.
|
|
783
|
+
]]
|
|
784
|
+
local params = if typeof(event.params) == "table" then event.params else nil
|
|
785
|
+
local title = Phrase.of(event.op, params)
|
|
786
|
+
local milliseconds = if typeof(event.ms) == "number" then event.ms else 0
|
|
787
|
+
local ok = event.ok == true
|
|
788
|
+
local detail = string.format("%s %.0fms", peerTag(event.from), milliseconds)
|
|
789
|
+
|
|
790
|
+
Console.beginCall(title, Phrase.kindOf(event.op))
|
|
791
|
+
Console.recordCall(ok, milliseconds)
|
|
792
|
+
if ok then
|
|
793
|
+
Console.log("reply", title, detail)
|
|
794
|
+
else
|
|
795
|
+
Console.log("error", string.format("%s failed", title), detail)
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
function connect()
|
|
801
|
+
local allowed, reason = canConnect()
|
|
802
|
+
if not allowed then
|
|
803
|
+
-- Reported as standby rather than an error: nothing failed, and this
|
|
804
|
+
-- session was never going to connect.
|
|
805
|
+
currentStatus = "disconnected"
|
|
806
|
+
Console.log("dim", "mirroring", reason)
|
|
807
|
+
Console.setStatus(
|
|
808
|
+
"mirroring",
|
|
809
|
+
string.format("%s build %s", "client view", Config.BUILD_ID)
|
|
810
|
+
)
|
|
811
|
+
Console.setCaption("mirroring the playtest server")
|
|
812
|
+
return
|
|
813
|
+
end
|
|
814
|
+
Transport.start(studioId(), { onCommand = onCommand, onStatus = onStatus, onEvent = onEvent })
|
|
815
|
+
plugin:SetSetting(SETTING_AUTOCONNECT, true)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
button.Click:Connect(function()
|
|
819
|
+
-- Flagged around the toggle so the Enabled handler knows this close is the
|
|
820
|
+
-- user's, and is safe to remember. See closingByHand.
|
|
821
|
+
closingByHand = widget.Enabled
|
|
822
|
+
widget.Enabled = not widget.Enabled
|
|
823
|
+
closingByHand = false
|
|
824
|
+
end)
|
|
825
|
+
|
|
826
|
+
plugin.Unloading:Connect(function()
|
|
827
|
+
Transport.stop()
|
|
828
|
+
end)
|
|
829
|
+
|
|
830
|
+
--[[
|
|
831
|
+
Switches this session between the push and long-poll transports.
|
|
832
|
+
|
|
833
|
+
Registered here rather than in a handler module because it is the only
|
|
834
|
+
command that has to reach back into the plugin object and the connection
|
|
835
|
+
loop, both of which live in this file -- and registered THIS far down the
|
|
836
|
+
file on purpose: `connect` is a forward-declared local, so a closure written
|
|
837
|
+
above its declaration captures the global of that name instead, which is nil.
|
|
838
|
+
That cost a session. The plugin loaded and connected perfectly, and then died
|
|
839
|
+
the first time somebody asked it to switch transport.
|
|
840
|
+
|
|
841
|
+
The reply is sent before the reconnect, and the reconnect is deferred:
|
|
842
|
+
tearing the stream down inside the handler would strand the answer to the
|
|
843
|
+
very call that asked for the switch, which is a confusing way to succeed.
|
|
844
|
+
]]
|
|
845
|
+
Dispatch.registerAll("studio", {
|
|
846
|
+
transport = function(params: { [string]: any }): { [string]: any }
|
|
847
|
+
local requested = params.mode
|
|
848
|
+
if requested ~= nil and requested ~= "sse" and requested ~= "poll" then
|
|
849
|
+
Dispatch.fail("BAD_PARAMS", 'transport mode must be "sse" or "poll".')
|
|
850
|
+
end
|
|
851
|
+
if requested == nil then
|
|
852
|
+
return { mode = Transport.getMode(), forcePoll = Transport.getForcePoll(), changed = false }
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
local wantPoll = requested == "poll"
|
|
856
|
+
if Transport.getMode() == requested and Transport.getForcePoll() == wantPoll then
|
|
857
|
+
return { mode = Transport.getMode(), forcePoll = wantPoll, changed = false }
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
Transport.setForcePoll(wantPoll)
|
|
861
|
+
plugin:SetSetting(SETTING_FORCE_POLL, wantPoll)
|
|
862
|
+
task.defer(function()
|
|
863
|
+
Transport.stop()
|
|
864
|
+
task.wait(0.1)
|
|
865
|
+
connect()
|
|
866
|
+
end)
|
|
867
|
+
return {
|
|
868
|
+
mode = requested,
|
|
869
|
+
forcePoll = wantPoll,
|
|
870
|
+
changed = true,
|
|
871
|
+
note = "Reconnecting on the new transport; the next call will use it.",
|
|
872
|
+
}
|
|
873
|
+
end,
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
refreshMeta()
|
|
877
|
+
|
|
878
|
+
-- Connect on load unless the user explicitly disconnected last session.
|
|
879
|
+
if plugin:GetSetting(SETTING_AUTOCONNECT) ~= false then
|
|
880
|
+
connect()
|
|
881
|
+
else
|
|
882
|
+
Console.log("warn", "auto-connect disabled — press reconnect to start")
|
|
883
|
+
end
|