@el4cteo/rbx-studio-mcp 0.6.0 → 0.6.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.
Files changed (48) hide show
  1. package/README.md +4 -4
  2. package/dist/index.js +4 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/tools/anim.js +159 -0
  5. package/dist/tools/anim.js.map +1 -0
  6. package/dist/tools/character.js +95 -5
  7. package/dist/tools/character.js.map +1 -1
  8. package/dist/tools/data.js +173 -0
  9. package/dist/tools/data.js.map +1 -0
  10. package/dist/tools/device.js +77 -7
  11. package/dist/tools/device.js.map +1 -1
  12. package/dist/tools/discover.js +80 -4
  13. package/dist/tools/discover.js.map +1 -1
  14. package/dist/tools/exec.js +48 -1
  15. package/dist/tools/exec.js.map +1 -1
  16. package/dist/tools/input.js +35 -9
  17. package/dist/tools/input.js.map +1 -1
  18. package/dist/tools/perf.js +74 -7
  19. package/dist/tools/perf.js.map +1 -1
  20. package/dist/tools/scripts.js +51 -3
  21. package/dist/tools/scripts.js.map +1 -1
  22. package/dist/tools/world.js +317 -44
  23. package/dist/tools/world.js.map +1 -1
  24. package/package.json +74 -74
  25. package/plugin/src/Commands.luau +622 -562
  26. package/plugin/src/Config.luau +65 -65
  27. package/plugin/src/Console.luau +1909 -1843
  28. package/plugin/src/Emulation.luau +172 -0
  29. package/plugin/src/Phrase.luau +164 -16
  30. package/plugin/src/Png.luau +8 -4
  31. package/plugin/src/Serialize.luau +499 -327
  32. package/plugin/src/Undo.luau +94 -6
  33. package/plugin/src/handlers/Anim.luau +897 -0
  34. package/plugin/src/handlers/Assets.luau +587 -352
  35. package/plugin/src/handlers/Capture.luau +155 -20
  36. package/plugin/src/handlers/Character.luau +823 -361
  37. package/plugin/src/handlers/Data.luau +539 -0
  38. package/plugin/src/handlers/Device.luau +394 -139
  39. package/plugin/src/handlers/Discover.luau +685 -363
  40. package/plugin/src/handlers/Geometry.luau +127 -0
  41. package/plugin/src/handlers/Perf.luau +227 -0
  42. package/plugin/src/handlers/Scripts.luau +673 -539
  43. package/plugin/src/handlers/Session.luau +3 -0
  44. package/plugin/src/handlers/Viewport.luau +268 -0
  45. package/plugin/src/handlers/World.luau +89 -15
  46. package/plugin/src/init.server.luau +879 -832
  47. package/scripts/build-plugin.mjs +20 -0
  48. package/scripts/check-plugin.mjs +171 -124
@@ -1,139 +1,394 @@
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
- --[[
72
- Two different failures, and calling both NO_SUCH_DEVICE sends the
73
- caller to check a spelling that was never wrong.
74
-
75
- The simulator refuses outright while a playtest is running -- "cannot
76
- change device in current game state" -- and this handler is reachable
77
- from the playtest session, which is exactly where someone testing a
78
- phone HUD would call it. The device is settable from the EDIT session
79
- meanwhile, playtest or no playtest, so the fix is a sentence, not a
80
- spelling check.
81
- ]]
82
- local reason = tostring(err)
83
- if string.find(reason, "game state", 1, true) ~= nil then
84
- Dispatch.fail(
85
- "DEVICE_LOCKED",
86
- string.format("Studio will not switch to %q while a playtest is running.", id),
87
- "Send this to the EDIT session's studioId instead -- it changes the viewport for "
88
- .. "the running playtest too -- or set the device before pressing play."
89
- )
90
- end
91
- Dispatch.fail(
92
- "NO_SUCH_DEVICE",
93
- string.format("Could not switch to %q: %s", id, reason),
94
- "Device ids look like \"iphone_16\" or \"ipad_a16\"; call `list` for the full set."
95
- )
96
- end
97
-
98
- if typeof(params.orientation) == "string" and params.orientation ~= "" then
99
- local okOrient, orientErr = pcall(function()
100
- (StudioDeviceSimulatorService :: any):SetOrientationAsync(
101
- (Enum :: any).ScreenOrientation[params.orientation]
102
- )
103
- end)
104
- if not okOrient then
105
- Dispatch.fail(
106
- "BAD_PARAMS",
107
- string.format("Could not set orientation %q: %s", params.orientation, tostring(orientErr)),
108
- 'Use "LandscapeLeft", "LandscapeRight", "Portrait" or "Sensor".'
109
- )
110
- end
111
- end
112
-
113
- return Emulation.state()
114
- end
115
-
116
- function Device.stop(_params: { [string]: any }): { [string]: any }
117
- local ok, err = pcall(function()
118
- (StudioDeviceSimulatorService :: any):StopSimulationAsync()
119
- end)
120
- if not ok then
121
- Dispatch.fail("DEVICE_UNAVAILABLE", string.format("Could not stop emulation: %s", tostring(err)))
122
- end
123
- return Emulation.state()
124
- end
125
-
126
- function Device.state(_params: { [string]: any }): { [string]: any }
127
- return Emulation.state()
128
- end
129
-
130
- function Device.register()
131
- Dispatch.registerAll("device", {
132
- list = Device.list,
133
- set = Device.set,
134
- stop = Device.stop,
135
- state = Device.state,
136
- })
137
- end
138
-
139
- return Device
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
+ The same tool also shapes network traffic, which belongs here for the same
16
+ reason the two sit together in Studio's own toolbar: "what a player on that
17
+ device sees" is not only the shape of their screen. Most of the players this
18
+ is aimed at are on a phone connection, and a menu that works at 0ms is not
19
+ evidence that it works at 300 -- the loading spinner that never stops, the
20
+ button that fires twice, the HUD that pops in after the round has started are
21
+ all invisible until the network is slow enough to show them.
22
+
23
+ Reading the current state is `Emulation`'s job, shared with `screenshot` and
24
+ `studio_status` so the three cannot drift apart on what is being emulated.
25
+ ]]
26
+
27
+ local StudioDeviceSimulatorService = game:GetService("StudioDeviceSimulatorService")
28
+
29
+ local Dispatch = require(script.Parent.Parent.Dispatch)
30
+ local Emulation = require(script.Parent.Parent.Emulation)
31
+
32
+ local Device = {}
33
+
34
+ function Device.list(_params: { [string]: any }): { [string]: any }
35
+ local ok, list = pcall(function()
36
+ return (StudioDeviceSimulatorService :: any):GetDeviceListAsync()
37
+ end)
38
+ if not ok then
39
+ Dispatch.fail("DEVICE_UNAVAILABLE", string.format("Could not list devices: %s", tostring(list)))
40
+ end
41
+
42
+ --[[
43
+ The list is ids, not descriptions, and an id alone does not say whether
44
+ "generic_handheld_720" is a phone or a handheld console. The details come
45
+ from a second call per device, which is worth the round trip once because
46
+ the caller otherwise has to guess which id to ask for.
47
+ ]]
48
+ local devices: { { [string]: any } } = {}
49
+ for _, id in list :: { string } do
50
+ local okInfo, info = pcall(function()
51
+ return (StudioDeviceSimulatorService :: any):GetDeviceInfoAsync(id)
52
+ end)
53
+ if okInfo and typeof(info) == "table" then
54
+ local entry = info :: { [string]: any }
55
+ table.insert(devices, {
56
+ id = tostring(id),
57
+ name = tostring(entry.Name),
58
+ form = tostring(entry.DeviceForm):gsub("Enum%.DeviceForm%.", ""),
59
+ resolution = string.format("%dx%d", entry.Width or 0, entry.Height or 0),
60
+ })
61
+ else
62
+ table.insert(devices, { id = tostring(id) })
63
+ end
64
+ end
65
+
66
+ return { devices = devices, count = #devices, current = Emulation.state() }
67
+ end
68
+
69
+ function Device.set(params: { [string]: any }): { [string]: any }
70
+ local id = params.device
71
+ if typeof(id) ~= "string" or id == "" then
72
+ Dispatch.fail("BAD_PARAMS", "set needs a `device` id. Call `list` to see them.")
73
+ end
74
+
75
+ local ok, err = pcall(function()
76
+ (StudioDeviceSimulatorService :: any):SetDeviceAsync(id)
77
+ end)
78
+ if not ok then
79
+ --[[
80
+ Two different failures, and calling both NO_SUCH_DEVICE sends the
81
+ caller to check a spelling that was never wrong.
82
+
83
+ The simulator refuses outright while a playtest is running -- "cannot
84
+ change device in current game state" -- and this handler is reachable
85
+ from the playtest session, which is exactly where someone testing a
86
+ phone HUD would call it. The device is settable from the EDIT session
87
+ meanwhile, playtest or no playtest, so the fix is a sentence, not a
88
+ spelling check.
89
+ ]]
90
+ local reason = tostring(err)
91
+ if string.find(reason, "game state", 1, true) ~= nil then
92
+ Dispatch.fail(
93
+ "DEVICE_LOCKED",
94
+ string.format("Studio will not switch to %q while a playtest is running.", id),
95
+ "Send this to the EDIT session's studioId instead -- it changes the viewport for "
96
+ .. "the running playtest too -- or set the device before pressing play."
97
+ )
98
+ end
99
+ Dispatch.fail(
100
+ "NO_SUCH_DEVICE",
101
+ string.format("Could not switch to %q: %s", id, reason),
102
+ "Device ids look like \"iphone_16\" or \"ipad_a16\"; call `list` for the full set."
103
+ )
104
+ end
105
+
106
+ if typeof(params.orientation) == "string" and params.orientation ~= "" then
107
+ local okOrient, orientErr = pcall(function()
108
+ (StudioDeviceSimulatorService :: any):SetOrientationAsync(
109
+ (Enum :: any).ScreenOrientation[params.orientation]
110
+ )
111
+ end)
112
+ if not okOrient then
113
+ Dispatch.fail(
114
+ "BAD_PARAMS",
115
+ string.format("Could not set orientation %q: %s", params.orientation, tostring(orientErr)),
116
+ 'Use "LandscapeLeft", "LandscapeRight", "Portrait" or "Sensor".'
117
+ )
118
+ end
119
+ end
120
+
121
+ return Emulation.state()
122
+ end
123
+
124
+ --[[
125
+ Named connections, so a caller does not have to invent milliseconds.
126
+
127
+ Round numbers off real-world measurements rather than anything
128
+ authoritative: the point is a plausible bad connection to test against, and
129
+ an argument about whether 3G is 150ms or 180ms does not change what breaks.
130
+ `poor` is deliberately harsh -- crowded stadium wifi, a phone on one bar --
131
+ because the failures worth finding only appear at the far end.
132
+
133
+ Loss is the percentage of packets thrown away, and it is the field that finds
134
+ the interesting bugs: latency makes a game feel slow, loss makes it behave
135
+ wrongly.
136
+ ]]
137
+ local PRESETS: { [string]: { latency: number, jitter: number, loss: number } } = {
138
+ clear = { latency = 0, jitter = 0, loss = 0 },
139
+ wifi = { latency = 15, jitter = 5, loss = 0 },
140
+ ["4g"] = { latency = 60, jitter = 15, loss = 0.5 },
141
+ ["3g"] = { latency = 150, jitter = 40, loss = 2 },
142
+ poor = { latency = 400, jitter = 120, loss = 8 },
143
+ }
144
+
145
+ --[[
146
+ Percent in, fraction out.
147
+
148
+ `InboundNetworkLossPercent` does not take a percentage: it takes a fraction
149
+ and clamps at 0.5. Writing 2 for "2% loss" succeeds, lands at 0.5, and
150
+ degrades half the traffic -- a hundred times what was asked for, with no
151
+ error anywhere. Every caller of this file speaks percent, so the conversion
152
+ is here and only here.
153
+ ]]
154
+ local function lossFraction(percent: number): number
155
+ return math.clamp(percent, 0, Emulation.LOSS_LIMIT_PERCENT) / 100
156
+ end
157
+
158
+ local function presetNames(): string
159
+ local names: { string } = {}
160
+ for name in PRESETS do
161
+ table.insert(names, name)
162
+ end
163
+ table.sort(names)
164
+ return table.concat(names, ", ")
165
+ end
166
+
167
+ --[[
168
+ Writes one shaping property, and says so if it could not.
169
+
170
+ Collected rather than raised: a Studio missing one of these six should apply
171
+ the five it has and report the gap, because the alternative is a `network`
172
+ call that fails outright having already shaped traffic halfway.
173
+ ]]
174
+ local function apply(service: any, property: string, value: number, refused: { string })
175
+ local ok = pcall(function()
176
+ service[property] = value
177
+ end)
178
+ if not ok then
179
+ table.insert(refused, property)
180
+ end
181
+ end
182
+
183
+ --[[
184
+ Degrades the connection on purpose.
185
+
186
+ Everything is optional and everything is absolute: a call naming only `loss`
187
+ leaves the delays where they were, which is what makes it possible to add one
188
+ variable at a time and see which one breaks the thing. `preset` sets all
189
+ three at once and named fields override it, so `preset="3g" loss=10` reads as
190
+ "3G, on a bad day".
191
+
192
+ Direction defaults to both. Inbound alone is the player with a bad
193
+ connection; outbound alone is everyone else seeing that player late.
194
+ ]]
195
+ function Device.network(params: { [string]: any }): { [string]: any }
196
+ local service = Emulation.networkSettings()
197
+ if service == nil then
198
+ Dispatch.fail(
199
+ "NETWORK_UNAVAILABLE",
200
+ "This Studio has no NetworkSettings service to shape traffic with.",
201
+ "Update Studio; the simulation properties landed in September 2026."
202
+ )
203
+ end
204
+
205
+ local direction = string.lower(tostring(params.direction or "both"))
206
+ if direction ~= "in" and direction ~= "out" and direction ~= "both" then
207
+ Dispatch.fail("BAD_PARAMS", 'direction must be "in", "out" or "both".')
208
+ end
209
+
210
+ local latency: number? = nil
211
+ local jitter: number? = nil
212
+ local loss: number? = nil
213
+
214
+ if params.preset ~= nil then
215
+ local name = string.lower(tostring(params.preset))
216
+ local preset = PRESETS[name]
217
+ if preset == nil then
218
+ Dispatch.fail(
219
+ "BAD_PARAMS",
220
+ string.format("unknown preset %q", tostring(params.preset)),
221
+ string.format("Presets are: %s.", presetNames())
222
+ )
223
+ end
224
+ latency, jitter, loss = preset.latency, preset.jitter, preset.loss
225
+ -- "clear" is a whole-connection word, so it takes the memory cap with
226
+ -- it. Any other preset says nothing about memory and leaves it alone.
227
+ if name == "clear" then
228
+ apply(service, "EmulatedTotalMemoryInMB", 0, {})
229
+ Emulation.setMemoryCap(nil)
230
+ end
231
+ end
232
+
233
+ -- Named fields win over the preset, so one value can be pushed past it
234
+ -- without respelling the other two.
235
+ if params.latency ~= nil then
236
+ latency = tonumber(params.latency) or 0
237
+ end
238
+ if params.jitter ~= nil then
239
+ jitter = tonumber(params.jitter) or 0
240
+ end
241
+ if params.loss ~= nil then
242
+ loss = tonumber(params.loss) or 0
243
+ end
244
+
245
+ --[[
246
+ Clamped here, where it can be reported, rather than left to the engine,
247
+ which clamps silently. `capped` goes back in the reply so a caller asking
248
+ for 5 seconds of latency learns it got one, instead of reading 1000 in
249
+ the state and assuming it typed the number wrong.
250
+ ]]
251
+ local capped: { string } = {}
252
+ if latency ~= nil and latency > Emulation.DELAY_LIMIT then
253
+ table.insert(capped, string.format("latency %g -> %d", latency, Emulation.DELAY_LIMIT))
254
+ latency = Emulation.DELAY_LIMIT
255
+ end
256
+ if jitter ~= nil and jitter > Emulation.DELAY_LIMIT then
257
+ table.insert(capped, string.format("jitter %g -> %d", jitter, Emulation.DELAY_LIMIT))
258
+ jitter = Emulation.DELAY_LIMIT
259
+ end
260
+ if loss ~= nil and loss > Emulation.LOSS_LIMIT_PERCENT then
261
+ table.insert(capped, string.format("loss %g%% -> %g%%", loss, Emulation.LOSS_LIMIT_PERCENT))
262
+ loss = Emulation.LOSS_LIMIT_PERCENT
263
+ end
264
+ if latency ~= nil then
265
+ latency = math.max(latency, 0)
266
+ end
267
+ if jitter ~= nil then
268
+ jitter = math.max(jitter, 0)
269
+ end
270
+
271
+ local memory: number? = nil
272
+ if params.memory ~= nil then
273
+ --[[
274
+ A memory ceiling in megabytes, because the device half of this tool
275
+ is only half the truth about a cheap phone: it has a small screen AND
276
+ little memory, and the second is what makes a place unload textures
277
+ or fail to join at all. 0 turns the cap off.
278
+ ]]
279
+ memory = math.clamp(math.floor(tonumber(params.memory) or 0), 0, 65536)
280
+ end
281
+
282
+ if latency == nil and jitter == nil and loss == nil and memory == nil then
283
+ Dispatch.fail(
284
+ "BAD_PARAMS",
285
+ "network needs something to set.",
286
+ string.format(
287
+ "Pass a `preset` (%s), or any of `latency`, `jitter`, `loss` and `memory`.",
288
+ presetNames()
289
+ )
290
+ )
291
+ end
292
+
293
+ local refused: { string } = {}
294
+ local inbound = direction == "in" or direction == "both"
295
+ local outbound = direction == "out" or direction == "both"
296
+
297
+ if latency ~= nil then
298
+ if inbound then
299
+ apply(service, "InboundNetworkMinDelayMs", latency, refused)
300
+ end
301
+ if outbound then
302
+ apply(service, "OutboundNetworkMinDelayMs", latency, refused)
303
+ end
304
+ end
305
+ if jitter ~= nil then
306
+ if inbound then
307
+ apply(service, "InboundNetworkJitterMs", jitter, refused)
308
+ end
309
+ if outbound then
310
+ apply(service, "OutboundNetworkJitterMs", jitter, refused)
311
+ end
312
+ end
313
+ if loss ~= nil then
314
+ local fraction = lossFraction(loss)
315
+ if inbound then
316
+ apply(service, "InboundNetworkLossPercent", fraction, refused)
317
+ end
318
+ if outbound then
319
+ apply(service, "OutboundNetworkLossPercent", fraction, refused)
320
+ end
321
+ end
322
+ if memory ~= nil then
323
+ apply(service, "EmulatedTotalMemoryInMB", memory, refused)
324
+ Emulation.setMemoryCap(memory)
325
+ end
326
+
327
+ local state = Emulation.network()
328
+ state.direction = direction
329
+ if #refused > 0 then
330
+ state.refused = refused
331
+ end
332
+ if #capped > 0 then
333
+ state.capped = capped
334
+ end
335
+ return state
336
+ end
337
+
338
+ --[[
339
+ Stops emulating: the viewport AND the connection.
340
+
341
+ Both, because "stop" is one idea to the person asking and two APIs
342
+ underneath. Leaving shaped traffic behind after a stop is the exact failure
343
+ this tool warns about everywhere else -- invisible state outliving the
344
+ session that set it -- and it would be worse here, because the caller
345
+ believes they just turned emulation off.
346
+ ]]
347
+ function Device.stop(_params: { [string]: any }): { [string]: any }
348
+ local ok, err = pcall(function()
349
+ (StudioDeviceSimulatorService :: any):StopSimulationAsync()
350
+ end)
351
+
352
+ local service = Emulation.networkSettings()
353
+ if service ~= nil then
354
+ local ignored: { string } = {}
355
+ apply(service, "InboundNetworkMinDelayMs", 0, ignored)
356
+ apply(service, "InboundNetworkJitterMs", 0, ignored)
357
+ apply(service, "InboundNetworkLossPercent", 0, ignored)
358
+ apply(service, "OutboundNetworkMinDelayMs", 0, ignored)
359
+ apply(service, "OutboundNetworkJitterMs", 0, ignored)
360
+ apply(service, "OutboundNetworkLossPercent", 0, ignored)
361
+ apply(service, "EmulatedTotalMemoryInMB", 0, ignored)
362
+ Emulation.setMemoryCap(nil)
363
+ end
364
+
365
+ --[[
366
+ Raised after the network is cleared, not before.
367
+
368
+ `StopSimulationAsync` throws when no device was ever set, which is a
369
+ perfectly ordinary way to reach this function -- someone shaping traffic
370
+ without emulating a phone. Failing first would have left their connection
371
+ degraded by the call that was meant to restore it.
372
+ ]]
373
+ if not ok and Emulation.deviceId() ~= nil then
374
+ Dispatch.fail("DEVICE_UNAVAILABLE", string.format("Could not stop emulation: %s", tostring(err)))
375
+ end
376
+
377
+ return Emulation.state()
378
+ end
379
+
380
+ function Device.state(_params: { [string]: any }): { [string]: any }
381
+ return Emulation.state()
382
+ end
383
+
384
+ function Device.register()
385
+ Dispatch.registerAll("device", {
386
+ list = Device.list,
387
+ set = Device.set,
388
+ stop = Device.stop,
389
+ network = Device.network,
390
+ state = Device.state,
391
+ })
392
+ end
393
+
394
+ return Device