@el4cteo/rbx-studio-mcp 0.7.8 → 0.8.2
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 +175 -175
- package/dist/bridge/harness.js +14 -5
- package/dist/bridge/harness.js.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/apidump.js +62 -25
- package/dist/lib/apidump.js.map +1 -1
- package/dist/lib/format.js +43 -4
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/livedata.js +24 -1
- package/dist/lib/livedata.js.map +1 -1
- package/dist/lib/liveops.js +79 -0
- package/dist/lib/liveops.js.map +1 -1
- package/dist/lib/monetization.js +119 -0
- package/dist/lib/monetization.js.map +1 -0
- package/dist/lib/opencloud.js +16 -6
- package/dist/lib/opencloud.js.map +1 -1
- package/dist/lib/pluginbuild.js +10 -0
- package/dist/lib/pluginbuild.js.map +1 -1
- package/dist/lib/png.js +104 -0
- package/dist/lib/png.js.map +1 -1
- package/dist/resources.js +3 -2
- package/dist/resources.js.map +1 -1
- package/dist/tools/api.js +4 -3
- package/dist/tools/api.js.map +1 -1
- package/dist/tools/audio.js +1 -1
- package/dist/tools/audio.js.map +1 -1
- package/dist/tools/debug.js +2 -2
- package/dist/tools/debug.js.map +1 -1
- package/dist/tools/device.js +10 -9
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/discover.js +15 -4
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +4 -4
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/instances.js +32 -7
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +27 -5
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/playtest.js +35 -10
- package/dist/tools/playtest.js.map +1 -1
- package/dist/tools/screenshot.js +55 -11
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/scripts.js +4 -4
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/terrain.js +5 -5
- package/dist/tools/terrain.js.map +1 -1
- package/dist/tools/universe.js +136 -11
- package/dist/tools/universe.js.map +1 -1
- package/dist/tools/world.js +38 -15
- package/dist/tools/world.js.map +1 -1
- package/package.json +75 -75
- package/plugin/src/ClientRelay.luau +20 -2
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Emulation.luau +44 -16
- package/plugin/src/ExecRuntime.luau +168 -161
- package/plugin/src/LogBuffer.luau +65 -3
- package/plugin/src/Paths.luau +15 -4
- package/plugin/src/Phrase.luau +3 -20
- package/plugin/src/ScriptEdit.luau +14 -0
- package/plugin/src/Serialize.luau +17 -0
- package/plugin/src/TextEdit.luau +6 -0
- package/plugin/src/handlers/Api.luau +8 -0
- package/plugin/src/handlers/Assets.luau +27 -18
- package/plugin/src/handlers/Audio.luau +36 -15
- package/plugin/src/handlers/Capture.luau +219 -3
- package/plugin/src/handlers/Character.luau +38 -23
- package/plugin/src/handlers/Data.luau +12 -1
- package/plugin/src/handlers/Device.luau +38 -27
- package/plugin/src/handlers/Generate.luau +8 -5
- package/plugin/src/handlers/Geometry.luau +7 -5
- package/plugin/src/handlers/Perf.luau +1 -0
- package/plugin/src/handlers/Playtest.luau +40 -3
- package/plugin/src/handlers/Terrain.luau +9 -24
- package/plugin/src/handlers/Viewport.luau +4 -4
- package/plugin/src/handlers/World.luau +17 -1
- package/plugin/src/init.server.luau +0 -2
- package/scripts/test-tools.mjs +437 -216
- package/dist/tools/anim.js +0 -159
- package/dist/tools/anim.js.map +0 -1
- package/plugin/src/handlers/Anim.luau +0 -897
|
@@ -130,29 +130,21 @@ end
|
|
|
130
130
|
`poor` is deliberately harsh -- crowded stadium wifi, a phone on one bar --
|
|
131
131
|
because the failures worth finding only appear at the far end.
|
|
132
132
|
|
|
133
|
-
Loss is the percentage of packets thrown away
|
|
134
|
-
the
|
|
135
|
-
|
|
133
|
+
Loss is the percentage of packets thrown away. Roblox caps it at 0.5%, so
|
|
134
|
+
the presets climb to that ceiling rather than to real-world figures the
|
|
135
|
+
engine would clamp anyway.
|
|
136
136
|
]]
|
|
137
137
|
local PRESETS: { [string]: { latency: number, jitter: number, loss: number } } = {
|
|
138
138
|
clear = { latency = 0, jitter = 0, loss = 0 },
|
|
139
139
|
wifi = { latency = 15, jitter = 5, loss = 0 },
|
|
140
|
-
["4g"] = { latency = 60, jitter = 15, loss = 0.
|
|
141
|
-
["3g"] = { latency = 150, jitter = 40, loss =
|
|
142
|
-
poor = { latency = 400, jitter = 120, loss =
|
|
140
|
+
["4g"] = { latency = 60, jitter = 15, loss = 0.1 },
|
|
141
|
+
["3g"] = { latency = 150, jitter = 40, loss = 0.3 },
|
|
142
|
+
poor = { latency = 400, jitter = 120, loss = 0.5 },
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
--
|
|
146
|
-
|
|
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
|
|
145
|
+
-- Percent in, percent out: the property takes a percentage, capped at 0.5.
|
|
146
|
+
local function lossPercent(percent: number): number
|
|
147
|
+
return math.clamp(percent, 0, Emulation.LOSS_LIMIT_PERCENT)
|
|
156
148
|
end
|
|
157
149
|
|
|
158
150
|
local function presetNames(): string
|
|
@@ -180,6 +172,19 @@ local function apply(service: any, property: string, value: number, refused: { s
|
|
|
180
172
|
end
|
|
181
173
|
end
|
|
182
174
|
|
|
175
|
+
--[[
|
|
176
|
+
Takes the memory cap off by writing the machine's real memory back. 0 is
|
|
177
|
+
not "no cap" to the engine -- it is a device with no memory -- so nothing
|
|
178
|
+
here writes it.
|
|
179
|
+
]]
|
|
180
|
+
local function uncapMemory(service: any, refused: { string })
|
|
181
|
+
local real = Emulation.machineMemory()
|
|
182
|
+
if real ~= nil then
|
|
183
|
+
apply(service, "EmulatedTotalMemoryInMB", real, refused)
|
|
184
|
+
end
|
|
185
|
+
Emulation.setMemoryCap(nil)
|
|
186
|
+
end
|
|
187
|
+
|
|
183
188
|
--[[
|
|
184
189
|
Degrades the connection on purpose.
|
|
185
190
|
|
|
@@ -224,9 +229,8 @@ function Device.network(params: { [string]: any }): { [string]: any }
|
|
|
224
229
|
latency, jitter, loss = preset.latency, preset.jitter, preset.loss
|
|
225
230
|
-- "clear" is a whole-connection word, so it takes the memory cap with
|
|
226
231
|
-- it. Any other preset says nothing about memory and leaves it alone.
|
|
227
|
-
if name == "clear" then
|
|
228
|
-
|
|
229
|
-
Emulation.setMemoryCap(nil)
|
|
232
|
+
if name == "clear" and Emulation.memoryCap() ~= nil then
|
|
233
|
+
uncapMemory(service, {})
|
|
230
234
|
end
|
|
231
235
|
end
|
|
232
236
|
|
|
@@ -311,17 +315,21 @@ function Device.network(params: { [string]: any }): { [string]: any }
|
|
|
311
315
|
end
|
|
312
316
|
end
|
|
313
317
|
if loss ~= nil then
|
|
314
|
-
local
|
|
318
|
+
local percent = lossPercent(loss)
|
|
315
319
|
if inbound then
|
|
316
|
-
apply(service, "InboundNetworkLossPercent",
|
|
320
|
+
apply(service, "InboundNetworkLossPercent", percent, refused)
|
|
317
321
|
end
|
|
318
322
|
if outbound then
|
|
319
|
-
apply(service, "OutboundNetworkLossPercent",
|
|
323
|
+
apply(service, "OutboundNetworkLossPercent", percent, refused)
|
|
320
324
|
end
|
|
321
325
|
end
|
|
322
326
|
if memory ~= nil then
|
|
323
|
-
|
|
324
|
-
|
|
327
|
+
if memory > 0 then
|
|
328
|
+
apply(service, "EmulatedTotalMemoryInMB", memory, refused)
|
|
329
|
+
Emulation.setMemoryCap(memory)
|
|
330
|
+
else
|
|
331
|
+
uncapMemory(service, refused)
|
|
332
|
+
end
|
|
325
333
|
end
|
|
326
334
|
|
|
327
335
|
local state = Emulation.network()
|
|
@@ -358,8 +366,11 @@ function Device.stop(_params: { [string]: any }): { [string]: any }
|
|
|
358
366
|
apply(service, "OutboundNetworkMinDelayMs", 0, ignored)
|
|
359
367
|
apply(service, "OutboundNetworkJitterMs", 0, ignored)
|
|
360
368
|
apply(service, "OutboundNetworkLossPercent", 0, ignored)
|
|
361
|
-
|
|
362
|
-
|
|
369
|
+
-- Only a cap this plugin set is undone. Studio's own simulator restores
|
|
370
|
+
-- its value when it stops, and writing over that is what left 0 behind.
|
|
371
|
+
if Emulation.memoryCap() ~= nil then
|
|
372
|
+
uncapMemory(service, ignored)
|
|
373
|
+
end
|
|
363
374
|
end
|
|
364
375
|
|
|
365
376
|
--[[
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
]]
|
|
26
26
|
|
|
27
27
|
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
28
|
+
local Serialize = require(script.Parent.Parent.Serialize)
|
|
28
29
|
local Paths = require(script.Parent.Parent.Paths)
|
|
29
30
|
local Undo = require(script.Parent.Parent.Undo)
|
|
30
31
|
|
|
@@ -89,15 +90,17 @@ local function carryLook(source: MeshPart, model: Model)
|
|
|
89
90
|
end
|
|
90
91
|
end
|
|
91
92
|
|
|
93
|
+
-- Absent is nil; present but malformed fails, rather than being taken as absent
|
|
94
|
+
-- and quietly replaced by a default the caller never asked for.
|
|
92
95
|
local function parseVector(text: any): Vector3?
|
|
93
|
-
if
|
|
96
|
+
if text == nil or text == "" then
|
|
94
97
|
return nil
|
|
95
98
|
end
|
|
96
|
-
local
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
+
local ok, value = Serialize.parse(text, "Vector3")
|
|
100
|
+
if not ok then
|
|
101
|
+
Dispatch.fail("BAD_PARAMS", string.format("%q is not an \"x, y, z\" vector.", tostring(text)))
|
|
99
102
|
end
|
|
100
|
-
return
|
|
103
|
+
return value :: Vector3
|
|
101
104
|
end
|
|
102
105
|
|
|
103
106
|
local function service(): any
|
|
@@ -56,15 +56,17 @@ local function carryAppearance(source: BasePart, target: BasePart)
|
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
-- Absent is nil; present but malformed fails, rather than being taken as absent
|
|
60
|
+
-- and quietly replaced by a default the caller never asked for.
|
|
59
61
|
local function parseVector(text: any): Vector3?
|
|
60
|
-
if
|
|
62
|
+
if text == nil or text == "" then
|
|
61
63
|
return nil
|
|
62
64
|
end
|
|
63
|
-
local
|
|
64
|
-
if
|
|
65
|
-
|
|
65
|
+
local ok, value = Serialize.parse(text, "Vector3")
|
|
66
|
+
if not ok then
|
|
67
|
+
Dispatch.fail("BAD_PARAMS", string.format("%q is not an \"x, y, z\" vector.", tostring(text)))
|
|
66
68
|
end
|
|
67
|
-
return
|
|
69
|
+
return value :: Vector3
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
local function resolveParts(paths: any, what: string): { BasePart }
|
|
@@ -68,8 +68,12 @@ end
|
|
|
68
68
|
|
|
69
69
|
local function state(): { [string]: any }
|
|
70
70
|
local players = {}
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
-- Only while a test runs: straight after `stop` the editor still lists the
|
|
72
|
+
-- departing player for a moment, and the reply said one was present.
|
|
73
|
+
if RunService:IsRunning() then
|
|
74
|
+
for _, player in game:GetService("Players"):GetPlayers() do
|
|
75
|
+
table.insert(players, { name = player.Name, userId = player.UserId })
|
|
76
|
+
end
|
|
73
77
|
end
|
|
74
78
|
return {
|
|
75
79
|
playtestsAllowed = Playtest.isAllowed(),
|
|
@@ -121,13 +125,46 @@ end
|
|
|
121
125
|
|
|
122
126
|
function Playtest.control(params: { [string]: any }): { [string]: any }
|
|
123
127
|
local op = params.op
|
|
124
|
-
if op == "play" or op == "run" or op == "multiplayer" then requireAllowed() end
|
|
128
|
+
if op == "play" or op == "run" or op == "multiplayer" or op == "addPlayers" then requireAllowed() end
|
|
125
129
|
local before = state()
|
|
126
130
|
|
|
127
131
|
if op == "state" then
|
|
128
132
|
return { changed = false, state = before }
|
|
129
133
|
end
|
|
130
134
|
|
|
135
|
+
--[[
|
|
136
|
+
More players joining a test already running: staggered joins, lobby
|
|
137
|
+
fill-up, the race a second player hits that the first never does.
|
|
138
|
+
|
|
139
|
+
Served by the playtest's server session, the only place AddPlayers may be
|
|
140
|
+
called from; the MCP server routes it there. Returns straight away --
|
|
141
|
+
new clients take seconds to load, and the caller polls `state` for them.
|
|
142
|
+
]]
|
|
143
|
+
if op == "addPlayers" then
|
|
144
|
+
if RunService:IsEdit() or not RunService:IsServer() then
|
|
145
|
+
Dispatch.fail(
|
|
146
|
+
"NOT_RUNNING",
|
|
147
|
+
"Players can only be added from the server of a running test.",
|
|
148
|
+
"Start one with `playtest op=\"multiplayer\"` first."
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
local count = tonumber(params.players) or 1
|
|
152
|
+
if count < 1 or count > 8 then
|
|
153
|
+
Dispatch.fail("BAD_PARAMS", "players must be between 1 and 8.")
|
|
154
|
+
end
|
|
155
|
+
local ok, err = pcall(function()
|
|
156
|
+
service():AddPlayers(count)
|
|
157
|
+
end)
|
|
158
|
+
if not ok then
|
|
159
|
+
Dispatch.fail(
|
|
160
|
+
"ADD_PLAYERS_FAILED",
|
|
161
|
+
string.format("Studio refused to add players: %s", tostring(err)),
|
|
162
|
+
"AddPlayers works in a multiplayer test only, not in Play or Run."
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
return { changed = true, state = before }
|
|
166
|
+
end
|
|
167
|
+
|
|
131
168
|
--[[
|
|
132
169
|
Ending a test has to happen inside it.
|
|
133
170
|
|
|
@@ -319,43 +319,28 @@ end
|
|
|
319
319
|
What is there, without reading a voxel.
|
|
320
320
|
|
|
321
321
|
`CountCells` is the cheap answer to "does this place even use terrain",
|
|
322
|
-
which is the question worth asking before any of the above.
|
|
323
|
-
tells an agent where to aim without guessing at the origin.
|
|
322
|
+
which is the question worth asking before any of the above.
|
|
324
323
|
]]
|
|
325
324
|
function Terrain.stats(_params: { [string]: any }): { [string]: any }
|
|
326
325
|
local field = terrain()
|
|
327
326
|
|
|
328
327
|
--[[
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
`MaxExtents` is a Region3int16 and its corners are Vector3int16, which is
|
|
332
|
-
a type `Serialize.value` has no case for -- it would have fallen through
|
|
333
|
-
to the generic tail and reported whatever `tostring` makes of it. The
|
|
334
|
-
extents are also in VOXELS, not studs, which is the sort of unit
|
|
335
|
-
confusion that produces a fill a thousand studs from where it was meant,
|
|
336
|
-
so they are converted and named for what they are.
|
|
337
|
-
]]
|
|
338
|
-
--[[
|
|
339
|
-
`MaxExtents` is deliberately not reported.
|
|
328
|
+
No bounds, and no limit.
|
|
340
329
|
|
|
341
|
-
|
|
342
|
-
not: it
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
misreading of the name -- checked against a place with 2220 cells in a
|
|
346
|
-
160-stud patch, which returned exactly the same numbers as one with
|
|
347
|
-
none.
|
|
330
|
+
`MaxExtents` reads like the bounding box of the terrain that exists and is
|
|
331
|
+
not: it answers -32000..32000 on an empty place and a full one alike. Nor
|
|
332
|
+
is it a limit in studs -- a fill at 140,000 studs out still landed -- so
|
|
333
|
+
the `limitStuds = 32000` this used to report was wrong as well.
|
|
348
334
|
|
|
349
335
|
There is no engine call for the occupied box, and finding it means
|
|
350
336
|
reading every voxel in the world, which is far too expensive for a
|
|
351
|
-
question meant to be cheap. `cells`
|
|
352
|
-
matters: whether this place uses terrain at all.
|
|
337
|
+
question meant to be cheap. `cells` answers the one that matters.
|
|
353
338
|
]]
|
|
354
339
|
return {
|
|
355
340
|
cells = field:CountCells(),
|
|
356
341
|
waterColor = Serialize.value(field.WaterColor),
|
|
357
|
-
|
|
358
|
-
|
|
342
|
+
-- Through Serialize so a float32 reads 0.15, not 0.15000000596046448.
|
|
343
|
+
waterWaveSize = Serialize.value(field.WaterWaveSize),
|
|
359
344
|
}
|
|
360
345
|
end
|
|
361
346
|
|
|
@@ -92,7 +92,7 @@ function Viewport.raycast(params: { [string]: any }): { [string]: any }
|
|
|
92
92
|
position = Serialize.value(cast.Position),
|
|
93
93
|
normal = Serialize.value(cast.Normal),
|
|
94
94
|
distance = math.floor(cast.Distance * 1000 + 0.5) / 1000,
|
|
95
|
-
material =
|
|
95
|
+
material = cast.Material.Name,
|
|
96
96
|
}
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -287,9 +287,9 @@ function Viewport.camera(params: { [string]: any }): { [string]: any }
|
|
|
287
287
|
end
|
|
288
288
|
|
|
289
289
|
return {
|
|
290
|
-
position =
|
|
291
|
-
lookVector =
|
|
292
|
-
fieldOfView = view.FieldOfView,
|
|
290
|
+
position = Serialize.value(view.CFrame.Position),
|
|
291
|
+
lookVector = Serialize.value(view.CFrame.LookVector),
|
|
292
|
+
fieldOfView = Serialize.value(view.FieldOfView),
|
|
293
293
|
}
|
|
294
294
|
end
|
|
295
295
|
|
|
@@ -213,6 +213,19 @@ function World.collision(params: { [string]: any }): { [string]: any }
|
|
|
213
213
|
if typeof(paths) ~= "table" or #(paths :: { any }) == 0 then
|
|
214
214
|
Dispatch.fail("BAD_PARAMS", "assign needs a `paths` array.")
|
|
215
215
|
end
|
|
216
|
+
-- The engine takes any string here, registered or not, and a part in an
|
|
217
|
+
-- unregistered group collides as Default: a typo that changes nothing
|
|
218
|
+
-- and reports success. Measured, so refused up front.
|
|
219
|
+
local okRegistered, registered = pcall(function()
|
|
220
|
+
return world:IsCollisionGroupRegistered(name)
|
|
221
|
+
end)
|
|
222
|
+
if okRegistered and registered == false then
|
|
223
|
+
Dispatch.fail(
|
|
224
|
+
"NO_SUCH_GROUP",
|
|
225
|
+
string.format("No collision group %q is registered in %s.", name, Paths.of(world)),
|
|
226
|
+
'Create it first with action="create", or check the name with action="list".'
|
|
227
|
+
)
|
|
228
|
+
end
|
|
216
229
|
local assigned: { string } = {}
|
|
217
230
|
local _, undoable = Undo.record("MCPCollision", "MCP collision group", function()
|
|
218
231
|
for _, path in paths :: { string } do
|
|
@@ -228,7 +241,10 @@ function World.collision(params: { [string]: any }): { [string]: any }
|
|
|
228
241
|
end
|
|
229
242
|
end
|
|
230
243
|
end)
|
|
231
|
-
|
|
244
|
+
-- A model can hold thousands of parts; the count is the answer and a
|
|
245
|
+
-- sample of names is enough to see it landed where intended.
|
|
246
|
+
local sample = if #assigned > 20 then table.move(assigned, 1, 20, 1, {}) else assigned
|
|
247
|
+
return { group = name, assigned = #assigned, parts = sample, undoable = undoable, world = Paths.of(world) }
|
|
232
248
|
end
|
|
233
249
|
|
|
234
250
|
if action == "remove" then
|
|
@@ -42,7 +42,6 @@ local Viewport = require(script.handlers.Viewport)
|
|
|
42
42
|
local Input = require(script.handlers.Input)
|
|
43
43
|
local Device = require(script.handlers.Device)
|
|
44
44
|
local Data = require(script.handlers.Data)
|
|
45
|
-
local Anim = require(script.handlers.Anim)
|
|
46
45
|
local Api = require(script.handlers.Api)
|
|
47
46
|
local Scripts = require(script.handlers.Scripts)
|
|
48
47
|
local Session = require(script.handlers.Session)
|
|
@@ -197,7 +196,6 @@ Scripts.register()
|
|
|
197
196
|
Input.register()
|
|
198
197
|
Device.register()
|
|
199
198
|
Data.register()
|
|
200
|
-
Anim.register()
|
|
201
199
|
Api.register()
|
|
202
200
|
|
|
203
201
|
--[[
|