@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.
- package/README.md +4 -4
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/anim.js +159 -0
- package/dist/tools/anim.js.map +1 -0
- package/dist/tools/character.js +95 -5
- package/dist/tools/character.js.map +1 -1
- package/dist/tools/data.js +173 -0
- package/dist/tools/data.js.map +1 -0
- package/dist/tools/device.js +77 -7
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/discover.js +80 -4
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +48 -1
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/input.js +35 -9
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/perf.js +74 -7
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +51 -3
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/world.js +317 -44
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Commands.luau +622 -562
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Console.luau +1909 -1843
- package/plugin/src/Emulation.luau +172 -0
- package/plugin/src/Phrase.luau +164 -16
- package/plugin/src/Png.luau +8 -4
- package/plugin/src/Serialize.luau +499 -327
- package/plugin/src/Undo.luau +94 -6
- package/plugin/src/handlers/Anim.luau +897 -0
- package/plugin/src/handlers/Assets.luau +587 -352
- package/plugin/src/handlers/Capture.luau +155 -20
- package/plugin/src/handlers/Character.luau +823 -361
- package/plugin/src/handlers/Data.luau +539 -0
- package/plugin/src/handlers/Device.luau +394 -139
- package/plugin/src/handlers/Discover.luau +685 -363
- package/plugin/src/handlers/Geometry.luau +127 -0
- package/plugin/src/handlers/Perf.luau +227 -0
- package/plugin/src/handlers/Scripts.luau +673 -539
- package/plugin/src/handlers/Session.luau +3 -0
- package/plugin/src/handlers/Viewport.luau +268 -0
- package/plugin/src/handlers/World.luau +89 -15
- package/plugin/src/init.server.luau +879 -832
- package/scripts/build-plugin.mjs +20 -0
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|