@el4cteo/rbx-studio-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/dist/bridge/rpc.js +243 -0
  4. package/dist/bridge/rpc.js.map +1 -0
  5. package/dist/bridge/server.js +281 -0
  6. package/dist/bridge/server.js.map +1 -0
  7. package/dist/index.js +104 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/lib/apidump.js +269 -0
  10. package/dist/lib/apidump.js.map +1 -0
  11. package/dist/lib/errors.js +38 -0
  12. package/dist/lib/errors.js.map +1 -0
  13. package/dist/lib/format.js +191 -0
  14. package/dist/lib/format.js.map +1 -0
  15. package/dist/lib/pluginbuild.js +83 -0
  16. package/dist/lib/pluginbuild.js.map +1 -0
  17. package/dist/lib/png.js +84 -0
  18. package/dist/lib/png.js.map +1 -0
  19. package/dist/lib/protocol.js +22 -0
  20. package/dist/lib/protocol.js.map +1 -0
  21. package/dist/lib/tool.js +27 -0
  22. package/dist/lib/tool.js.map +1 -0
  23. package/dist/resources.js +70 -0
  24. package/dist/resources.js.map +1 -0
  25. package/dist/tools/api.js +78 -0
  26. package/dist/tools/api.js.map +1 -0
  27. package/dist/tools/character.js +94 -0
  28. package/dist/tools/character.js.map +1 -0
  29. package/dist/tools/debug.js +211 -0
  30. package/dist/tools/debug.js.map +1 -0
  31. package/dist/tools/device.js +74 -0
  32. package/dist/tools/device.js.map +1 -0
  33. package/dist/tools/discover.js +217 -0
  34. package/dist/tools/discover.js.map +1 -0
  35. package/dist/tools/exec.js +191 -0
  36. package/dist/tools/exec.js.map +1 -0
  37. package/dist/tools/input.js +96 -0
  38. package/dist/tools/input.js.map +1 -0
  39. package/dist/tools/instances.js +261 -0
  40. package/dist/tools/instances.js.map +1 -0
  41. package/dist/tools/perf.js +367 -0
  42. package/dist/tools/perf.js.map +1 -0
  43. package/dist/tools/playtest.js +153 -0
  44. package/dist/tools/playtest.js.map +1 -0
  45. package/dist/tools/screenshot.js +75 -0
  46. package/dist/tools/screenshot.js.map +1 -0
  47. package/dist/tools/scripts.js +316 -0
  48. package/dist/tools/scripts.js.map +1 -0
  49. package/dist/tools/session.js +152 -0
  50. package/dist/tools/session.js.map +1 -0
  51. package/dist/tools/world.js +281 -0
  52. package/dist/tools/world.js.map +1 -0
  53. package/package.json +62 -0
  54. package/plugin/default.project.json +6 -0
  55. package/plugin/src/Config.luau +59 -0
  56. package/plugin/src/Console.luau +657 -0
  57. package/plugin/src/Context.luau +35 -0
  58. package/plugin/src/Dispatch.luau +90 -0
  59. package/plugin/src/Editor.luau +142 -0
  60. package/plugin/src/Emulation.luau +151 -0
  61. package/plugin/src/LogBuffer.luau +277 -0
  62. package/plugin/src/Net.luau +102 -0
  63. package/plugin/src/Paths.luau +255 -0
  64. package/plugin/src/Phrase.luau +465 -0
  65. package/plugin/src/Png.luau +238 -0
  66. package/plugin/src/Scope.luau +78 -0
  67. package/plugin/src/ScriptEdit.luau +100 -0
  68. package/plugin/src/Serialize.luau +287 -0
  69. package/plugin/src/TextEdit.luau +296 -0
  70. package/plugin/src/Transport.luau +328 -0
  71. package/plugin/src/Undo.luau +72 -0
  72. package/plugin/src/Visuals.luau +710 -0
  73. package/plugin/src/handlers/Api.luau +242 -0
  74. package/plugin/src/handlers/Assets.luau +145 -0
  75. package/plugin/src/handlers/Capture.luau +187 -0
  76. package/plugin/src/handlers/Character.luau +361 -0
  77. package/plugin/src/handlers/Debug.luau +391 -0
  78. package/plugin/src/handlers/Device.luau +119 -0
  79. package/plugin/src/handlers/Discover.luau +289 -0
  80. package/plugin/src/handlers/Exec.luau +270 -0
  81. package/plugin/src/handlers/Geometry.luau +261 -0
  82. package/plugin/src/handlers/Input.luau +287 -0
  83. package/plugin/src/handlers/Instances.luau +389 -0
  84. package/plugin/src/handlers/Perf.luau +645 -0
  85. package/plugin/src/handlers/Playtest.luau +205 -0
  86. package/plugin/src/handlers/Scripts.luau +387 -0
  87. package/plugin/src/handlers/Session.luau +168 -0
  88. package/plugin/src/handlers/Viewport.luau +302 -0
  89. package/plugin/src/handlers/World.luau +176 -0
  90. package/plugin/src/init.server.luau +317 -0
  91. package/scripts/build-plugin.mjs +157 -0
  92. package/scripts/check-plugin.mjs +97 -0
  93. package/scripts/install-plugin.mjs +39 -0
  94. package/scripts/latency.mjs +201 -0
  95. package/scripts/locate-luau.mjs +51 -0
  96. package/scripts/sourcemap.mjs +58 -0
  97. package/scripts/test-plugin.mjs +82 -0
@@ -0,0 +1,391 @@
1
+ --!strict
2
+ --[[
3
+ Breakpoints and runtime inspection, through `ScriptDebuggerService`.
4
+
5
+ Not `DebuggerManager`, which is the legacy service and refuses plugins
6
+ outright -- it wants the LocalUser capability. ScriptDebuggerService is
7
+ PluginSecurity throughout and is what Roblox shipped to replace it.
8
+
9
+ Two things shape this design, both learned rather than assumed.
10
+
11
+ The service reports "ScriptDebuggerService was never initialized" until a
12
+ callback is attached, in an editor session and a running playtest alike. So
13
+ `OnStopped` is installed before anything else is attempted.
14
+
15
+ And `OnStopped` must return its resume decision synchronously. It cannot yield
16
+ waiting for an agent to look at the stack and decide what to do, which rules
17
+ out interactive stepping over a request/response protocol entirely. What works
18
+ instead is a tracepoint: the callback captures the stack and variables into a
19
+ buffer, lets execution continue, and the agent reads the snapshots afterwards.
20
+ For finding out what a value was at a moment in time -- which is what a
21
+ debugger is usually reached for -- that is as good, and it does not leave the
22
+ user's Studio frozen mid-frame.
23
+
24
+ The documented return shape is contradictory -- the API dump says the callback
25
+ returns a Dictionary, Roblox's own announcement returns
26
+ `Enum.DebuggerResumeType.Resume` directly -- and this file used to hedge by
27
+ never stopping at all, which is worth recording because the hedge cost the
28
+ whole feature. `ContinueExecution = true` means the debugger does not stop;
29
+ not stopping means `OnStopped` never runs; and `OnStopped` is the only place
30
+ a stack or a variable is readable. Every capture breakpoint verified, fired,
31
+ and recorded nothing, while logpoints on the same lines printed normally --
32
+ so the half that needed no callback worked and hid that the other half was
33
+ dead.
34
+
35
+ The enum is the right return. It resumes cleanly: a script with a breakpoint
36
+ mid-loop ran to completion untouched, so stopping to capture costs the run
37
+ nothing and strands nothing.
38
+
39
+ There is no third option. `DebuggerResumeType` offers StepInto, StepOut,
40
+ StepOver and Resume, and none of them means "stay stopped", so a breakpoint
41
+ that holds a thread for someone to look at is not expressible here -- which
42
+ is why nothing in this file offers one.
43
+ ]]
44
+
45
+ local ScriptDebuggerService = game:GetService("ScriptDebuggerService")
46
+
47
+ local Dispatch = require(script.Parent.Parent.Dispatch)
48
+ local Paths = require(script.Parent.Parent.Paths)
49
+
50
+ -- Snapshots are far heavier than log lines: each carries a stack and its
51
+ -- variables. A tight cap keeps a breakpoint inside a loop from exhausting memory
52
+ -- before anyone reads it.
53
+ local MAX_SNAPSHOTS = 40
54
+ local MAX_FRAMES = 12
55
+ local MAX_VARIABLES = 40
56
+
57
+ local Debug = {}
58
+
59
+ local service = ScriptDebuggerService :: any
60
+
61
+ local snapshots: { { [string]: any } } = {}
62
+ local overflow = 0
63
+ local installed = false
64
+ local installError: string? = nil
65
+
66
+ --[[
67
+ Flattens whatever the service hands back.
68
+
69
+ Every one of these shapes is undocumented, so nothing is indexed by a guessed
70
+ field name. Keys are taken as they come and values stringified, which means an
71
+ unfamiliar shape arrives readable instead of arriving empty.
72
+ ]]
73
+ local function flatten(value: any, depth: number): any
74
+ if depth > 3 then
75
+ return "<nested>"
76
+ end
77
+ local kind = typeof(value)
78
+ if kind ~= "table" then
79
+ if kind == "Instance" then
80
+ return (value :: Instance):GetFullName()
81
+ end
82
+ if kind == "EnumItem" then
83
+ return tostring(value)
84
+ end
85
+ return if kind == "string" or kind == "number" or kind == "boolean"
86
+ then value
87
+ else tostring(value)
88
+ end
89
+
90
+ local source = value :: { [any]: any }
91
+ local out: { [string]: any } = {}
92
+ local count = 0
93
+ for key, item in source do
94
+ count += 1
95
+ if count > MAX_VARIABLES then
96
+ out["..."] = "more"
97
+ break
98
+ end
99
+ out[tostring(key)] = flatten(item, depth + 1)
100
+ end
101
+ return out
102
+ end
103
+
104
+ --[[
105
+ Builds the record for one stop.
106
+
107
+ Each lookup is guarded on its own. A stop that yields a thread id but no
108
+ readable variables is still worth keeping, and losing the whole snapshot to
109
+ one failed call would waste the only moment the data existed.
110
+ ]]
111
+ local function capture(stopped: any)
112
+ local record: { [string]: any } = {
113
+ at = os.time(),
114
+ stopped = flatten(stopped, 0),
115
+ }
116
+
117
+ --[[
118
+ `ThreadIds`, plural, holding an array -- measured from a real stop:
119
+
120
+ { ThreadIds = {3002}, Reason = Enum.ScriptStoppedReason.Breakpoint }
121
+
122
+ Reading `ThreadId` instead cost nothing visible: snapshots still arrived,
123
+ carrying the reason and nothing else, so a breakpoint looked like it was
124
+ working while the stack and variables it exists to collect were never
125
+ fetched.
126
+ ]]
127
+ local threadId: any = nil
128
+ if typeof(stopped) == "table" then
129
+ local ids = (stopped :: any).ThreadIds
130
+ threadId = if typeof(ids) == "table" then (ids :: { any })[1] else (stopped :: any).ThreadId
131
+ end
132
+
133
+ if threadId ~= nil then
134
+ local ok, trace = pcall(function()
135
+ return service:GetStackTrace(threadId)
136
+ end)
137
+ if ok then
138
+ record.stack = flatten(trace, 0)
139
+
140
+ -- Variables hang off a frame, so they are only reachable once the
141
+ -- trace has given up a frame id.
142
+ local frames = if typeof(trace) == "table" then (trace :: any).Frames else nil
143
+ if typeof(frames) == "table" then
144
+ local locals: { any } = {}
145
+ for index, frame in frames :: { any } do
146
+ if index > MAX_FRAMES then
147
+ break
148
+ end
149
+ -- `Id`, measured: a frame is {Id, Line, Name, ScriptPath}. The
150
+ -- StackFrame *instance* used by Studio's own debugger calls
151
+ -- it FrameId, which is what this read first, and the
152
+ -- mismatch cost only the variables -- the frame itself still
153
+ -- listed, so the stack looked complete.
154
+ local raw = frame :: any
155
+ local frameId = if typeof(frame) == "table" then (raw.Id or raw.FrameId) else nil
156
+ if frameId ~= nil then
157
+ local gotVars, vars = pcall(function()
158
+ return service:GetRootVariables(frameId)
159
+ end)
160
+ table.insert(locals, {
161
+ frame = flatten(frame, 1),
162
+ variables = if gotVars then flatten(vars, 1) else tostring(vars),
163
+ })
164
+ end
165
+ end
166
+ record.frames = locals
167
+ end
168
+ else
169
+ record.stackError = tostring(trace)
170
+ end
171
+ end
172
+
173
+ if #snapshots >= MAX_SNAPSHOTS then
174
+ table.remove(snapshots, 1)
175
+ overflow += 1
176
+ end
177
+ table.insert(snapshots, record)
178
+ end
179
+
180
+ --[[
181
+ Attaches the callback, which is also what initialises the service.
182
+
183
+ The resume decision follows Roblox's own example and returns the enum rather
184
+ than the Dictionary the dump describes. That is settled by observation now,
185
+ not by preference: breakpoints stop, this returns, and the stopped script
186
+ carries on to its last line.
187
+ ]]
188
+ local function install(): boolean
189
+ if installed then
190
+ return true
191
+ end
192
+
193
+ local ok, err = pcall(function()
194
+ service.OnStopped = function(stopped: any): any
195
+ -- Capture must never be what keeps a thread paused.
196
+ pcall(capture, stopped)
197
+ return Enum.DebuggerResumeType.Resume
198
+ end
199
+ end)
200
+
201
+ if not ok then
202
+ installError = tostring(err)
203
+ return false
204
+ end
205
+ installed = true
206
+ installError = nil
207
+ return true
208
+ end
209
+
210
+ local function requireService()
211
+ if not install() then
212
+ Dispatch.fail(
213
+ "NO_DEBUGGER",
214
+ string.format(
215
+ "ScriptDebuggerService is not available in this session: %s",
216
+ tostring(installError)
217
+ ),
218
+ "It is a beta API; the Studio build may not expose it yet."
219
+ )
220
+ end
221
+ end
222
+
223
+ function Debug.set(params: { [string]: any }): { [string]: any }
224
+ requireService()
225
+
226
+ local requested = params.breakpoints
227
+ if typeof(requested) ~= "table" then
228
+ Dispatch.fail("BAD_PARAMS", "debug set requires a `breakpoints` array.")
229
+ end
230
+
231
+ local added: { { [string]: any } } = {}
232
+ local failed: { { [string]: any } } = {}
233
+
234
+ for _, item in requested :: { { [string]: any } } do
235
+ local target = Paths.resolve(item.path)
236
+ if not target:IsA("LuaSourceContainer") then
237
+ Dispatch.fail(
238
+ "NOT_A_SCRIPT",
239
+ string.format("%s is a %s, not a script.", item.path, target.ClassName)
240
+ )
241
+ end
242
+
243
+ --[[
244
+ Capitalised keys, from Roblox's example -- `Line`, `Condition`,
245
+ `LogMessage`, `ContinueExecution`. Lowercase `line` was rejected.
246
+
247
+ `ContinueExecution` decides whether the debugger stops at all, and
248
+ stopping is the only thing that raises `OnStopped` -- which is the
249
+ only place a stack or a variable can be read. So a breakpoint that
250
+ continues past itself captures nothing, ever.
251
+
252
+ That was this file's default, on the reasoning that not stopping was
253
+ the safe choice: a wrong return from the callback could strand a
254
+ paused thread, and never pausing made that impossible. It also made
255
+ the feature impossible. Measured on a line proven to fire -- the same
256
+ breakpoint, same condition, differing only here -- it captured
257
+ nothing across two sessions, while the identical breakpoint carrying
258
+ a LogMessage printed on cue.
259
+
260
+ Stopping is not the hazard it was assumed to be. `OnStopped` returns
261
+ `Resume` and the thread continues on its own: the probe script ran to
262
+ completion, printing every line after the breakpoint, with no one
263
+ touching Studio. So capture stops, and a LogMessage -- which the
264
+ engine prints without help -- does not need to.
265
+ ]]
266
+ local logMessage = if typeof(item.logMessage) == "string" and item.logMessage ~= ""
267
+ then item.logMessage
268
+ else nil
269
+ local descriptor: { [string]: any } = {
270
+ Line = tonumber(item.line),
271
+ ContinueExecution = logMessage ~= nil,
272
+ }
273
+ if typeof(item.condition) == "string" and item.condition ~= "" then
274
+ descriptor.Condition = item.condition
275
+ end
276
+ if logMessage ~= nil then
277
+ descriptor.LogMessage = logMessage
278
+ end
279
+
280
+ local ok, result = pcall(function()
281
+ return service:AddBreakpoint(target, descriptor)
282
+ end)
283
+
284
+ if ok then
285
+ table.insert(added, {
286
+ path = Paths.of(target),
287
+ line = descriptor.Line,
288
+ -- What it will do when hit, since the two kinds behave nothing
289
+ -- alike: one writes a line to the output, the other stops long
290
+ -- enough to read the stack and then resumes itself.
291
+ mode = if logMessage ~= nil then "log" else "capture",
292
+ result = flatten(result, 1),
293
+ })
294
+ else
295
+ table.insert(failed, { path = item.path, line = descriptor.Line, error = tostring(result) })
296
+ end
297
+ end
298
+
299
+ return { added = added, failed = failed, installed = installed }
300
+ end
301
+
302
+ function Debug.clear(params: { [string]: any }): { [string]: any }
303
+ requireService()
304
+
305
+ local path = params.path
306
+ if typeof(path) == "string" and path ~= "" then
307
+ local target = Paths.resolve(path)
308
+ local line = tonumber(params.line)
309
+ if line == nil then
310
+ Dispatch.fail("BAD_PARAMS", "removing one breakpoint needs a `line`.")
311
+ end
312
+ local ok, removed = pcall(function()
313
+ return service:RemoveBreakpoint(target, line)
314
+ end)
315
+ return { removed = ok and removed == true, path = Paths.of(target), line = line }
316
+ end
317
+
318
+ local ok, err = pcall(function()
319
+ service:ClearBreakpoints()
320
+ end)
321
+ if not ok then
322
+ Dispatch.fail("REFUSED", string.format("ClearBreakpoints refused: %s", tostring(err)))
323
+ end
324
+ return { cleared = true }
325
+ end
326
+
327
+ function Debug.snapshots(params: { [string]: any }): { [string]: any }
328
+ local limit = math.clamp(tonumber(params.limit) or 10, 1, MAX_SNAPSHOTS)
329
+
330
+ local out: { { [string]: any } } = {}
331
+ local first = math.max(1, #snapshots - limit + 1)
332
+ for index = first, #snapshots do
333
+ table.insert(out, snapshots[index])
334
+ end
335
+
336
+ if params.clear == true then
337
+ snapshots = {}
338
+ overflow = 0
339
+ end
340
+
341
+ return {
342
+ items = out,
343
+ total = #snapshots,
344
+ overflow = if overflow > 0 then overflow else nil,
345
+ installed = installed,
346
+ }
347
+ end
348
+
349
+ function Debug.exceptions(params: { [string]: any }): { [string]: any }
350
+ requireService()
351
+
352
+ local mode = tostring(params.mode or "Unhandled")
353
+ local item = (Enum.DebugBreakModeType :: any)[mode]
354
+ if item == nil then
355
+ Dispatch.fail("BAD_PARAMS", string.format("unknown break mode %q", mode))
356
+ end
357
+
358
+ local ok, err = pcall(function()
359
+ service:SetExceptionBreakMode(item)
360
+ end)
361
+ if not ok then
362
+ Dispatch.fail("REFUSED", string.format("SetExceptionBreakMode refused: %s", tostring(err)))
363
+ end
364
+ return { mode = mode }
365
+ end
366
+
367
+ function Debug.register()
368
+ --[[
369
+ Installed at load, not on the first debug request.
370
+
371
+ A breakpoint is registered in one session and hit in another: the editor
372
+ holds it, the playtest's DataModel runs the code and raises the stop. That
373
+ second session is created by pressing play, long after any tool call
374
+ reached the first, so waiting for a request to install the callback leaves
375
+ exactly the session that does the stopping without one -- and a breakpoint
376
+ that verifies, fires, and records nothing.
377
+
378
+ Attaching it here costs a callback assignment per session and means every
379
+ session is ready before anything needs it.
380
+ ]]
381
+ install()
382
+
383
+ Dispatch.registerAll("debug", {
384
+ set = Debug.set,
385
+ clear = Debug.clear,
386
+ snapshots = Debug.snapshots,
387
+ exceptions = Debug.exceptions,
388
+ })
389
+ end
390
+
391
+ return Debug
@@ -0,0 +1,119 @@
1
+ --!strict
2
+ --[[
3
+ Device emulation: run Studio's viewport as a phone, tablet or console.
4
+
5
+ Most Roblox players are on a phone, and most UI is authored on a desktop
6
+ monitor. The gap between those two is where interfaces break -- a button
7
+ inside the notch, a menu off the bottom of a 393-pixel-tall screen, text
8
+ scaled for a display three times larger. Reading the data model cannot find
9
+ any of it, because every one of those instances has correct properties.
10
+
11
+ `StudioDeviceSimulatorService` resizes the viewport for real. Paired with
12
+ `screenshot` it is the only way to answer "does this work on a phone"
13
+ without owning one: set the device, take the picture, look.
14
+
15
+ Reading the current state is `Emulation`'s job, shared with `screenshot` and
16
+ `studio_status` so the three cannot drift apart on what is being emulated.
17
+ ]]
18
+
19
+ local StudioDeviceSimulatorService = game:GetService("StudioDeviceSimulatorService")
20
+
21
+ local Dispatch = require(script.Parent.Parent.Dispatch)
22
+ local Emulation = require(script.Parent.Parent.Emulation)
23
+
24
+ local Device = {}
25
+
26
+ function Device.list(_params: { [string]: any }): { [string]: any }
27
+ local ok, list = pcall(function()
28
+ return (StudioDeviceSimulatorService :: any):GetDeviceListAsync()
29
+ end)
30
+ if not ok then
31
+ Dispatch.fail("DEVICE_UNAVAILABLE", string.format("Could not list devices: %s", tostring(list)))
32
+ end
33
+
34
+ --[[
35
+ The list is ids, not descriptions, and an id alone does not say whether
36
+ "generic_handheld_720" is a phone or a handheld console. The details come
37
+ from a second call per device, which is worth the round trip once because
38
+ the caller otherwise has to guess which id to ask for.
39
+ ]]
40
+ local devices: { { [string]: any } } = {}
41
+ for _, id in list :: { string } do
42
+ local okInfo, info = pcall(function()
43
+ return (StudioDeviceSimulatorService :: any):GetDeviceInfoAsync(id)
44
+ end)
45
+ if okInfo and typeof(info) == "table" then
46
+ local entry = info :: { [string]: any }
47
+ table.insert(devices, {
48
+ id = tostring(id),
49
+ name = tostring(entry.Name),
50
+ form = tostring(entry.DeviceForm):gsub("Enum%.DeviceForm%.", ""),
51
+ resolution = string.format("%dx%d", entry.Width or 0, entry.Height or 0),
52
+ })
53
+ else
54
+ table.insert(devices, { id = tostring(id) })
55
+ end
56
+ end
57
+
58
+ return { devices = devices, count = #devices, current = Emulation.state() }
59
+ end
60
+
61
+ function Device.set(params: { [string]: any }): { [string]: any }
62
+ local id = params.device
63
+ if typeof(id) ~= "string" or id == "" then
64
+ Dispatch.fail("BAD_PARAMS", "set needs a `device` id. Call `list` to see them.")
65
+ end
66
+
67
+ local ok, err = pcall(function()
68
+ (StudioDeviceSimulatorService :: any):SetDeviceAsync(id)
69
+ end)
70
+ if not ok then
71
+ Dispatch.fail(
72
+ "NO_SUCH_DEVICE",
73
+ string.format("Could not switch to %q: %s", id, tostring(err)),
74
+ "Device ids look like \"iphone_16\" or \"ipad_a16\"; call `list` for the full set."
75
+ )
76
+ end
77
+
78
+ if typeof(params.orientation) == "string" and params.orientation ~= "" then
79
+ local okOrient, orientErr = pcall(function()
80
+ (StudioDeviceSimulatorService :: any):SetOrientationAsync(
81
+ (Enum :: any).ScreenOrientation[params.orientation]
82
+ )
83
+ end)
84
+ if not okOrient then
85
+ Dispatch.fail(
86
+ "BAD_PARAMS",
87
+ string.format("Could not set orientation %q: %s", params.orientation, tostring(orientErr)),
88
+ 'Use "LandscapeLeft", "LandscapeRight", "Portrait" or "Sensor".'
89
+ )
90
+ end
91
+ end
92
+
93
+ return Emulation.state()
94
+ end
95
+
96
+ function Device.stop(_params: { [string]: any }): { [string]: any }
97
+ local ok, err = pcall(function()
98
+ (StudioDeviceSimulatorService :: any):StopSimulationAsync()
99
+ end)
100
+ if not ok then
101
+ Dispatch.fail("DEVICE_UNAVAILABLE", string.format("Could not stop emulation: %s", tostring(err)))
102
+ end
103
+ return Emulation.state()
104
+ end
105
+
106
+ function Device.state(_params: { [string]: any }): { [string]: any }
107
+ return Emulation.state()
108
+ end
109
+
110
+ function Device.register()
111
+ Dispatch.registerAll("device", {
112
+ list = Device.list,
113
+ set = Device.set,
114
+ stop = Device.stop,
115
+ state = Device.state,
116
+ })
117
+ end
118
+
119
+ return Device