@el4cteo/rbx-studio-mcp 0.6.5 → 0.6.7
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 +235 -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 +122 -3
- package/dist/tools/data.js.map +1 -1
- package/dist/tools/exec.js +48 -1
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/scripts.js +112 -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 +177 -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 +1 -1
- 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
package/plugin/src/Phrase.luau
CHANGED
|
@@ -1,766 +1,816 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
Turns a command into something a person can read at a glance.
|
|
4
|
-
|
|
5
|
-
The console used to print the wire name: `-> script.edit`, `-> instances.create`.
|
|
6
|
-
That tells you the plugin is alive and nothing else. Watching an agent work,
|
|
7
|
-
the interesting question is always "what is it touching" -- and the answer is
|
|
8
|
-
sitting right there in the parameters, one field away from being shown.
|
|
9
|
-
|
|
10
|
-
So `script.edit` on ServerScriptService.KillBrick becomes "Edit KillBrick",
|
|
11
|
-
and `instances.create` with eight children becomes "Create 8 instances in
|
|
12
|
-
Workspace". The subject is what the user recognises; the op is what the
|
|
13
|
-
protocol needed.
|
|
14
|
-
|
|
15
|
-
Three rules hold this together:
|
|
16
|
-
|
|
17
|
-
Name the subject, not the path. "KillBrick" is what someone called their
|
|
18
|
-
script; "ServerScriptService.Systems.KillBrick" is where it lives, and it
|
|
19
|
-
buries the recognisable part behind the part that never changes.
|
|
20
|
-
|
|
21
|
-
Count when there are several. "Edit 4 scripts" is worth reading. Listing
|
|
22
|
-
four paths in a 76-pixel-wide log line is not.
|
|
23
|
-
|
|
24
|
-
Never invent. An unknown op falls back to a tidied version of its own name
|
|
25
|
-
rather than a guess, because a console that lies about what ran is worse
|
|
26
|
-
than one that reads awkwardly.
|
|
27
|
-
]]
|
|
28
|
-
|
|
29
|
-
local Phrase = {}
|
|
30
|
-
|
|
31
|
-
-- The tail of a dotted path is the bit a human named. Everything before it is
|
|
32
|
-
-- Roblox's filing system.
|
|
33
|
-
local function leaf(path: any): string?
|
|
34
|
-
if typeof(path) ~= "string" or path == "" then
|
|
35
|
-
return nil
|
|
36
|
-
end
|
|
37
|
-
local last = string.match(path, "([^%.]+)$")
|
|
38
|
-
if last == nil or last == "" then
|
|
39
|
-
return nil
|
|
40
|
-
end
|
|
41
|
-
-- Array indexing survives into the name -- "Part[3]" is meaningfully
|
|
42
|
-
-- different from "Part" when there are several siblings.
|
|
43
|
-
return last
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
local function count(value: any): number
|
|
47
|
-
if typeof(value) ~= "table" then
|
|
48
|
-
return 0
|
|
49
|
-
end
|
|
50
|
-
return #(value :: { any })
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
--[[
|
|
54
|
-
"1 script" / "4 scripts", without the caller assembling it each time.
|
|
55
|
-
]]
|
|
56
|
-
local function plural(n: number, singular: string, pluralForm: string?): string
|
|
57
|
-
local word = if n == 1 then singular else (pluralForm or (singular .. "s"))
|
|
58
|
-
return string.format("%d %s", n, word)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
--[[
|
|
62
|
-
Describes a list of paths: the name when there is one, a count when there are
|
|
63
|
-
several. This is the shape most batch tools take.
|
|
64
|
-
]]
|
|
65
|
-
local function subjectOf(paths: any, noun: string): string
|
|
66
|
-
local total = count(paths)
|
|
67
|
-
if total == 1 then
|
|
68
|
-
local name = leaf((paths :: { any })[1])
|
|
69
|
-
if name ~= nil then
|
|
70
|
-
return name
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
if total > 1 then
|
|
74
|
-
return plural(total, noun)
|
|
75
|
-
end
|
|
76
|
-
return noun
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
--[[
|
|
80
|
-
Capitalises an op fragment for the fallback. `snapshots` -> `Snapshots`.
|
|
81
|
-
]]
|
|
82
|
-
local function titleCase(value: string): string
|
|
83
|
-
return (string.upper(string.sub(value, 1, 1)) .. string.sub(value, 2))
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
type Describer = (params: { [string]: any }) -> string
|
|
87
|
-
|
|
88
|
-
--[[
|
|
89
|
-
One entry per op. Each reads its own parameters, because what identifies a
|
|
90
|
-
call differs completely between them: a script edit is about a path, a
|
|
91
|
-
playtest is about a mode, a find is about what was searched for.
|
|
92
|
-
]]
|
|
93
|
-
local DESCRIBERS: { [string]: Describer } = {
|
|
94
|
-
["script.read"] = function(params)
|
|
95
|
-
return "Read " .. subjectOf(params.paths, "script")
|
|
96
|
-
end,
|
|
97
|
-
["script.edit"] = function(params)
|
|
98
|
-
local edits = params.edits
|
|
99
|
-
local total = count(edits)
|
|
100
|
-
if total == 1 then
|
|
101
|
-
local name = leaf((edits :: { any })[1].path)
|
|
102
|
-
if name ~= nil then
|
|
103
|
-
return "Edit " .. name
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
-- Several edits often land on one script, and "Edit 3 scripts" would be
|
|
107
|
-
-- wrong when it is three changes to one file.
|
|
108
|
-
local seen: { [string]: boolean } = {}
|
|
109
|
-
local distinct = 0
|
|
110
|
-
for _, edit in (edits or {}) :: { { [string]: any } } do
|
|
111
|
-
local name = leaf(edit.path)
|
|
112
|
-
if name ~= nil and not seen[name] then
|
|
113
|
-
seen[name] = true
|
|
114
|
-
distinct += 1
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
if distinct == 1 then
|
|
118
|
-
for name in seen do
|
|
119
|
-
return string.format("Edit %s (%s)", name, plural(total, "change"))
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
return string.format("Edit %s", plural(distinct, "script"))
|
|
123
|
-
end,
|
|
124
|
-
["script.create"] = function(params)
|
|
125
|
-
local scripts = params.scripts
|
|
126
|
-
if count(scripts) == 1 then
|
|
127
|
-
local one = (scripts :: { any })[1]
|
|
128
|
-
local className = if typeof(one.className) == "string" then one.className else "Script"
|
|
129
|
-
return string.format("Create %s %s", one.name or "script", className)
|
|
130
|
-
end
|
|
131
|
-
return "Create " .. plural(count(scripts), "script")
|
|
132
|
-
end,
|
|
133
|
-
["script.grep"] = function(params)
|
|
134
|
-
local pattern = if typeof(params.pattern) == "string" then params.pattern else "?"
|
|
135
|
-
return string.format('Search scripts for "%s"', pattern)
|
|
136
|
-
end,
|
|
137
|
-
|
|
138
|
-
--[[
|
|
139
|
-
Names the class, not just whatever the caller happened to call the
|
|
140
|
-
thing. "Create MainMenu in StarterGui" was true and useless -- a name is
|
|
141
|
-
whatever someone typed, and it says nothing about what actually landed
|
|
142
|
-
in the place. `className` is required by the tool itself, so it is
|
|
143
|
-
always there; the chosen name rides along in quotes when it was given,
|
|
144
|
-
the way script.grep already quotes a search term.
|
|
145
|
-
]]
|
|
146
|
-
["instances.create"] = function(params)
|
|
147
|
-
local instances = params.instances
|
|
148
|
-
local total = count(instances)
|
|
149
|
-
if total == 1 then
|
|
150
|
-
local one = (instances :: { any })[1]
|
|
151
|
-
local className = if typeof(one.className) == "string" and one.className ~= ""
|
|
152
|
-
then one.className
|
|
153
|
-
else "instance"
|
|
154
|
-
local subject = if typeof(one.name) == "string" and one.name ~= ""
|
|
155
|
-
then string.format('%s "%s"', className, one.name)
|
|
156
|
-
else className
|
|
157
|
-
local where = leaf(one.parent)
|
|
158
|
-
return if where ~= nil
|
|
159
|
-
then string.format("Create %s in %s", subject, where)
|
|
160
|
-
else "Create " .. subject
|
|
161
|
-
end
|
|
162
|
-
local where = if total > 0 then leaf((instances :: { any })[1].parent) else nil
|
|
163
|
-
return if where ~= nil
|
|
164
|
-
then string.format("Create %s in %s", plural(total, "instance"), where)
|
|
165
|
-
else "Create " .. plural(total, "instance")
|
|
166
|
-
end,
|
|
167
|
-
["instances.modify"] = function(params)
|
|
168
|
-
local targets = params.targets
|
|
169
|
-
-- Each target carries its own path list, so the honest count is the
|
|
170
|
-
-- total across them rather than the number of entries.
|
|
171
|
-
local total = 0
|
|
172
|
-
local onlyName: string? = nil
|
|
173
|
-
--[[
|
|
174
|
-
Which properties/attributes actually changed, not just how many
|
|
175
|
-
instances did. "Modify Sword" said something happened and nothing
|
|
176
|
-
about what; a caller watching for "did the color change" had no way
|
|
177
|
-
to tell this apart from a rename or an attribute flip.
|
|
178
|
-
]]
|
|
179
|
-
local fields: { string } = {}
|
|
180
|
-
local seenFields: { [string]: boolean } = {}
|
|
181
|
-
local function noteFields(bag: any)
|
|
182
|
-
if typeof(bag) ~= "table" then
|
|
183
|
-
return
|
|
184
|
-
end
|
|
185
|
-
for key in bag :: { [string]: any } do
|
|
186
|
-
if not seenFields[key] then
|
|
187
|
-
seenFields[key] = true
|
|
188
|
-
table.insert(fields, key)
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
for _, target in (targets or {}) :: { { [string]: any } } do
|
|
193
|
-
local paths = count(target.paths)
|
|
194
|
-
total += paths
|
|
195
|
-
if paths >= 1 and onlyName == nil then
|
|
196
|
-
onlyName = leaf((target.paths :: { any })[1])
|
|
197
|
-
end
|
|
198
|
-
noteFields(target.properties)
|
|
199
|
-
noteFields(target.attributes)
|
|
200
|
-
end
|
|
201
|
-
if total == 1 and onlyName ~= nil then
|
|
202
|
-
if #fields == 1 then
|
|
203
|
-
return string.format("Set %s on %s", fields[1], onlyName)
|
|
204
|
-
elseif #fields > 1 then
|
|
205
|
-
return string.format("Modify %s (%s)", onlyName, table.concat(fields, ", "))
|
|
206
|
-
end
|
|
207
|
-
return "Modify " .. onlyName
|
|
208
|
-
end
|
|
209
|
-
if #fields == 1 then
|
|
210
|
-
return string.format("Set %s on %s", fields[1], plural(total, "instance"))
|
|
211
|
-
end
|
|
212
|
-
return "Modify " .. plural(total, "instance")
|
|
213
|
-
end,
|
|
214
|
-
["instances.delete"] = function(params)
|
|
215
|
-
return "Delete " .. subjectOf(params.paths, "instance")
|
|
216
|
-
end,
|
|
217
|
-
["instances.move"] = function(params)
|
|
218
|
-
local items = params.items
|
|
219
|
-
local total = count(items)
|
|
220
|
-
local cloning = total > 0 and (items :: { any })[1].mode == "clone"
|
|
221
|
-
local verb = if cloning then "Clone" else "Move"
|
|
222
|
-
if total == 1 then
|
|
223
|
-
local one = (items :: { any })[1]
|
|
224
|
-
local what = leaf(one.path)
|
|
225
|
-
local where = leaf(one.to)
|
|
226
|
-
-- A rename in the same call changes what actually ends up in the
|
|
227
|
-
-- place; "Clone Sword to Backpack" while it lands as "GoldSword"
|
|
228
|
-
-- described an instance that was never created under that name.
|
|
229
|
-
local rename = if typeof(one.name) == "string" and one.name ~= "" then one.name else nil
|
|
230
|
-
if what ~= nil and where ~= nil then
|
|
231
|
-
local base = string.format("%s %s to %s", verb, what, where)
|
|
232
|
-
return if rename ~= nil then base .. " as " .. rename else base
|
|
233
|
-
end
|
|
234
|
-
if what ~= nil then
|
|
235
|
-
return if rename ~= nil
|
|
236
|
-
then string.format("%s %s as %s", verb, what, rename)
|
|
237
|
-
else verb .. " " .. what
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
return string.format("%s %s", verb, plural(total, "instance"))
|
|
241
|
-
end,
|
|
242
|
-
|
|
243
|
-
["discover.tree"] = function(params)
|
|
244
|
-
local where = leaf(params.path)
|
|
245
|
-
return if where ~= nil then "Browse " .. where else "Browse the place"
|
|
246
|
-
end,
|
|
247
|
-
["discover.inspect"] = function(params)
|
|
248
|
-
--[[
|
|
249
|
-
`inspect` (no explicit `properties`, not concise detail) and `modify`
|
|
250
|
-
(when setting properties) both probe a class before doing the thing
|
|
251
|
-
the caller actually asked for -- reading the properties that matter
|
|
252
|
-
for it, or typing the values it is about to write. That probe is a
|
|
253
|
-
second, genuine call to this same op, and unmarked it logged
|
|
254
|
-
identically to the read it precedes: two lines reading "Inspect X"
|
|
255
|
-
back to back, indistinguishable from asking twice by mistake.
|
|
256
|
-
]]
|
|
257
|
-
if params.probeOnly == true then
|
|
258
|
-
return "Check the class of " .. subjectOf(params.paths, "instance")
|
|
259
|
-
end
|
|
260
|
-
return "Inspect " .. subjectOf(params.paths, "instance")
|
|
261
|
-
end,
|
|
262
|
-
["discover.find"] = function(params)
|
|
263
|
-
if typeof(params.tag) == "string" and params.tag ~= "" then
|
|
264
|
-
return string.format('Find things tagged "%s"', params.tag)
|
|
265
|
-
end
|
|
266
|
-
if typeof(params.nameContains) == "string" and params.nameContains ~= "" then
|
|
267
|
-
return string.format('Find "%s"', params.nameContains)
|
|
268
|
-
end
|
|
269
|
-
if typeof(params.className) == "string" and params.className ~= "" then
|
|
270
|
-
return string.format("Find every %s", params.className)
|
|
271
|
-
end
|
|
272
|
-
return "Search the place"
|
|
273
|
-
end,
|
|
274
|
-
|
|
275
|
-
["debug.set"] = function(params)
|
|
276
|
-
local breakpoints = params.breakpoints
|
|
277
|
-
local total = count(breakpoints)
|
|
278
|
-
if total == 1 then
|
|
279
|
-
local one = (breakpoints :: { any })[1]
|
|
280
|
-
local name = leaf(one.path)
|
|
281
|
-
if name ~= nil then
|
|
282
|
-
return string.format("Break on %s:%s", name, tostring(one.line))
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
return "Set " .. plural(total, "breakpoint")
|
|
286
|
-
end,
|
|
287
|
-
["debug.clear"] = function(params)
|
|
288
|
-
local name = leaf(params.path)
|
|
289
|
-
return if name ~= nil then "Clear breakpoints in " .. name else "Clear all breakpoints"
|
|
290
|
-
end,
|
|
291
|
-
["debug.snapshots"] = function()
|
|
292
|
-
return "Read what the breakpoints caught"
|
|
293
|
-
end,
|
|
294
|
-
["debug.exceptions"] = function(params)
|
|
295
|
-
return string.format("Break on errors: %s", tostring(params.mode or "Unhandled"))
|
|
296
|
-
end,
|
|
297
|
-
|
|
298
|
-
["perf.console"] = function(params)
|
|
299
|
-
if typeof(params.level) == "string" and params.level ~= "" then
|
|
300
|
-
return string.format("Read the %s log", params.level)
|
|
301
|
-
end
|
|
302
|
-
if typeof(params.pattern) == "string" and params.pattern ~= "" then
|
|
303
|
-
return string.format('Search output for "%s"', params.pattern)
|
|
304
|
-
end
|
|
305
|
-
return "Read the output log"
|
|
306
|
-
end,
|
|
307
|
-
["perf.snapshot"] = function()
|
|
308
|
-
return "Measure performance"
|
|
309
|
-
end,
|
|
310
|
-
["perf.profile"] = function(params)
|
|
311
|
-
return string.format("Profile scripts for %ss", tostring(params.seconds or 5))
|
|
312
|
-
end,
|
|
313
|
-
["perf.coverage"] = function(params)
|
|
314
|
-
local total = count(params.enable)
|
|
315
|
-
if total > 0 then
|
|
316
|
-
return "Instrument " .. plural(total, "script")
|
|
317
|
-
end
|
|
318
|
-
return "Read line coverage"
|
|
319
|
-
end,
|
|
320
|
-
|
|
321
|
-
["playtest.control"] = function(params)
|
|
322
|
-
local op = tostring(params.op or "state")
|
|
323
|
-
--[[
|
|
324
|
-
`stop` ends a test by polling this from the *surviving* session every
|
|
325
|
-
400ms until the teardown settles, sometimes five or six times in two
|
|
326
|
-
seconds. Labelled "Check the playtest" like an agent's own status
|
|
327
|
-
check, that read as unexplained repetition with no `stop` line anywhere
|
|
328
|
-
to explain it -- the actual stop went to the session that is by then
|
|
329
|
-
gone, and its console went with it. This name is why: waiting for the
|
|
330
|
-
teardown it just started, not asking again for no reason.
|
|
331
|
-
]]
|
|
332
|
-
if op == "state" and params.waitingForStop == true then
|
|
333
|
-
return "Wait for the playtest to stop"
|
|
334
|
-
end
|
|
335
|
-
if op == "play" then
|
|
336
|
-
return "Start a playtest"
|
|
337
|
-
elseif op == "run" then
|
|
338
|
-
return "Run the place"
|
|
339
|
-
elseif op == "multiplayer" then
|
|
340
|
-
return string.format("Start a %s test", plural(tonumber(params.players) or 2, "player"))
|
|
341
|
-
elseif op == "stop" then
|
|
342
|
-
return "Stop the playtest"
|
|
343
|
-
elseif op == "endTest" then
|
|
344
|
-
return "End the test"
|
|
345
|
-
end
|
|
346
|
-
return "Check the playtest"
|
|
347
|
-
end,
|
|
348
|
-
|
|
349
|
-
["exec.run"] = function(params)
|
|
350
|
-
local source = if typeof(params.source) == "string" then params.source else ""
|
|
351
|
-
|
|
352
|
-
--[[
|
|
353
|
-
Says what the code is about, not what its first line says.
|
|
354
|
-
|
|
355
|
-
This used to show the opening line verbatim, which produced entries
|
|
356
|
-
like "Run: local GenerationService = game:GetServi..." -- a fragment
|
|
357
|
-
of a variable declaration, cut mid-word, telling the reader nothing
|
|
358
|
-
except that some code ran. The declaration is never the point of a
|
|
359
|
-
snippet; it is the setup before it.
|
|
360
|
-
|
|
361
|
-
So the services it touches stand in for it. That is the one thing a
|
|
362
|
-
snippet reliably says about itself, it is short, and it is what
|
|
363
|
-
someone glancing at the console actually wants: which part of Studio
|
|
364
|
-
is being poked.
|
|
365
|
-
]]
|
|
366
|
-
local services: { string } = {}
|
|
367
|
-
local seen: { [string]: boolean } = {}
|
|
368
|
-
for name in string.gmatch(source, 'GetService%("([%w_]+)"%)') do
|
|
369
|
-
if not seen[name] then
|
|
370
|
-
seen[name] = true
|
|
371
|
-
table.insert(services, name)
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
local lines = 0
|
|
376
|
-
for _ in string.gmatch(source, "[^\r\n]+") do
|
|
377
|
-
lines += 1
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
if #services == 1 then
|
|
381
|
-
return "Run Luau on " .. services[1]
|
|
382
|
-
elseif #services == 2 then
|
|
383
|
-
return string.format("Run Luau on %s and %s", services[1], services[2])
|
|
384
|
-
elseif #services > 2 then
|
|
385
|
-
return string.format("Run Luau on %s and %d more", services[1], #services - 1)
|
|
386
|
-
end
|
|
387
|
-
return if lines <= 1 then "Run Luau" else string.format("Run Luau (%d lines)", lines)
|
|
388
|
-
end,
|
|
389
|
-
|
|
390
|
-
["viewport.select"] = function(params)
|
|
391
|
-
local total = count(params.paths)
|
|
392
|
-
if total == 0 then
|
|
393
|
-
return "Clear the selection"
|
|
394
|
-
end
|
|
395
|
-
return "Select " .. subjectOf(params.paths, "instance")
|
|
396
|
-
end,
|
|
397
|
-
["viewport.raycast"] = function()
|
|
398
|
-
return "Cast a ray"
|
|
399
|
-
end,
|
|
400
|
-
|
|
401
|
-
["capture.screenshot"] = function()
|
|
402
|
-
return "Take a screenshot"
|
|
403
|
-
end,
|
|
404
|
-
["capture.playtestId"] = function()
|
|
405
|
-
return "Capture the playtest view"
|
|
406
|
-
end,
|
|
407
|
-
["capture.decode"] = function()
|
|
408
|
-
return "Read back the capture"
|
|
409
|
-
end,
|
|
410
|
-
|
|
411
|
-
["viewport.focus"] = function(params)
|
|
412
|
-
local name = leaf(params.path)
|
|
413
|
-
return if name ~= nil then "Look at " .. name else "Move the camera"
|
|
414
|
-
end,
|
|
415
|
-
["viewport.camera"] = function()
|
|
416
|
-
return "Move the camera"
|
|
417
|
-
end,
|
|
418
|
-
|
|
419
|
-
["geometry.combine"] = function(params)
|
|
420
|
-
local op = tostring(params.op or "union")
|
|
421
|
-
local name = leaf(params.path)
|
|
422
|
-
local verb = if op == "subtract" then "Cut" elseif op == "intersect" then "Intersect" else "Merge"
|
|
423
|
-
if op == "subtract" and name ~= nil then
|
|
424
|
-
local from = count(params.with)
|
|
425
|
-
return string.format("Cut %s out of %s", plural(from, "shape"), name)
|
|
426
|
-
end
|
|
427
|
-
return if name ~= nil then string.format("%s %s", verb, name) else verb .. " parts"
|
|
428
|
-
end,
|
|
429
|
-
["geometry.fragment"] = function(params)
|
|
430
|
-
local name = leaf(params.path)
|
|
431
|
-
return if name ~= nil
|
|
432
|
-
then string.format("Shatter %s into %s", name, plural(tonumber(params.pieces) or 8, "piece"))
|
|
433
|
-
else "Shatter a part"
|
|
434
|
-
end,
|
|
435
|
-
|
|
436
|
-
["assets.insert"] = function(params)
|
|
437
|
-
return string.format("Insert asset %s", tostring(params.assetId or "?"))
|
|
438
|
-
end,
|
|
439
|
-
|
|
440
|
-
["world.history"] = function(params)
|
|
441
|
-
local action = tostring(params.action or "status")
|
|
442
|
-
if action == "status" then
|
|
443
|
-
return "Check the undo history"
|
|
444
|
-
end
|
|
445
|
-
local steps = tonumber(params.steps) or 1
|
|
446
|
-
return string.format("%s %s", if action == "undo" then "Undo" else "Redo", plural(steps, "step"))
|
|
447
|
-
end,
|
|
448
|
-
["world.collision"] = function(params)
|
|
449
|
-
local action = tostring(params.action or "list")
|
|
450
|
-
local group = if typeof(params.group) == "string" then params.group else "?"
|
|
451
|
-
if action == "list" then
|
|
452
|
-
return "List collision groups"
|
|
453
|
-
elseif action == "create" then
|
|
454
|
-
return "Create collision group " .. group
|
|
455
|
-
elseif action == "assign" then
|
|
456
|
-
return string.format("Put %s in %s", plural(count(params.paths), "instance"), group)
|
|
457
|
-
elseif action == "remove" then
|
|
458
|
-
return "Remove collision group " .. group
|
|
459
|
-
end
|
|
460
|
-
return string.format("Set %s against %s", group, tostring(params.with or "?"))
|
|
461
|
-
end,
|
|
462
|
-
|
|
463
|
-
["
|
|
464
|
-
local
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return
|
|
470
|
-
end,
|
|
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
|
-
return
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
[
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
return "
|
|
596
|
-
end,
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
["
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
end,
|
|
609
|
-
|
|
610
|
-
["
|
|
611
|
-
local
|
|
612
|
-
return if
|
|
613
|
-
end,
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
["
|
|
619
|
-
local
|
|
620
|
-
return if
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
end,
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
["
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
end,
|
|
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
|
-
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Turns a command into something a person can read at a glance.
|
|
4
|
+
|
|
5
|
+
The console used to print the wire name: `-> script.edit`, `-> instances.create`.
|
|
6
|
+
That tells you the plugin is alive and nothing else. Watching an agent work,
|
|
7
|
+
the interesting question is always "what is it touching" -- and the answer is
|
|
8
|
+
sitting right there in the parameters, one field away from being shown.
|
|
9
|
+
|
|
10
|
+
So `script.edit` on ServerScriptService.KillBrick becomes "Edit KillBrick",
|
|
11
|
+
and `instances.create` with eight children becomes "Create 8 instances in
|
|
12
|
+
Workspace". The subject is what the user recognises; the op is what the
|
|
13
|
+
protocol needed.
|
|
14
|
+
|
|
15
|
+
Three rules hold this together:
|
|
16
|
+
|
|
17
|
+
Name the subject, not the path. "KillBrick" is what someone called their
|
|
18
|
+
script; "ServerScriptService.Systems.KillBrick" is where it lives, and it
|
|
19
|
+
buries the recognisable part behind the part that never changes.
|
|
20
|
+
|
|
21
|
+
Count when there are several. "Edit 4 scripts" is worth reading. Listing
|
|
22
|
+
four paths in a 76-pixel-wide log line is not.
|
|
23
|
+
|
|
24
|
+
Never invent. An unknown op falls back to a tidied version of its own name
|
|
25
|
+
rather than a guess, because a console that lies about what ran is worse
|
|
26
|
+
than one that reads awkwardly.
|
|
27
|
+
]]
|
|
28
|
+
|
|
29
|
+
local Phrase = {}
|
|
30
|
+
|
|
31
|
+
-- The tail of a dotted path is the bit a human named. Everything before it is
|
|
32
|
+
-- Roblox's filing system.
|
|
33
|
+
local function leaf(path: any): string?
|
|
34
|
+
if typeof(path) ~= "string" or path == "" then
|
|
35
|
+
return nil
|
|
36
|
+
end
|
|
37
|
+
local last = string.match(path, "([^%.]+)$")
|
|
38
|
+
if last == nil or last == "" then
|
|
39
|
+
return nil
|
|
40
|
+
end
|
|
41
|
+
-- Array indexing survives into the name -- "Part[3]" is meaningfully
|
|
42
|
+
-- different from "Part" when there are several siblings.
|
|
43
|
+
return last
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
local function count(value: any): number
|
|
47
|
+
if typeof(value) ~= "table" then
|
|
48
|
+
return 0
|
|
49
|
+
end
|
|
50
|
+
return #(value :: { any })
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
--[[
|
|
54
|
+
"1 script" / "4 scripts", without the caller assembling it each time.
|
|
55
|
+
]]
|
|
56
|
+
local function plural(n: number, singular: string, pluralForm: string?): string
|
|
57
|
+
local word = if n == 1 then singular else (pluralForm or (singular .. "s"))
|
|
58
|
+
return string.format("%d %s", n, word)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
--[[
|
|
62
|
+
Describes a list of paths: the name when there is one, a count when there are
|
|
63
|
+
several. This is the shape most batch tools take.
|
|
64
|
+
]]
|
|
65
|
+
local function subjectOf(paths: any, noun: string): string
|
|
66
|
+
local total = count(paths)
|
|
67
|
+
if total == 1 then
|
|
68
|
+
local name = leaf((paths :: { any })[1])
|
|
69
|
+
if name ~= nil then
|
|
70
|
+
return name
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
if total > 1 then
|
|
74
|
+
return plural(total, noun)
|
|
75
|
+
end
|
|
76
|
+
return noun
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
--[[
|
|
80
|
+
Capitalises an op fragment for the fallback. `snapshots` -> `Snapshots`.
|
|
81
|
+
]]
|
|
82
|
+
local function titleCase(value: string): string
|
|
83
|
+
return (string.upper(string.sub(value, 1, 1)) .. string.sub(value, 2))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
type Describer = (params: { [string]: any }) -> string
|
|
87
|
+
|
|
88
|
+
--[[
|
|
89
|
+
One entry per op. Each reads its own parameters, because what identifies a
|
|
90
|
+
call differs completely between them: a script edit is about a path, a
|
|
91
|
+
playtest is about a mode, a find is about what was searched for.
|
|
92
|
+
]]
|
|
93
|
+
local DESCRIBERS: { [string]: Describer } = {
|
|
94
|
+
["script.read"] = function(params)
|
|
95
|
+
return "Read " .. subjectOf(params.paths, "script")
|
|
96
|
+
end,
|
|
97
|
+
["script.edit"] = function(params)
|
|
98
|
+
local edits = params.edits
|
|
99
|
+
local total = count(edits)
|
|
100
|
+
if total == 1 then
|
|
101
|
+
local name = leaf((edits :: { any })[1].path)
|
|
102
|
+
if name ~= nil then
|
|
103
|
+
return "Edit " .. name
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
-- Several edits often land on one script, and "Edit 3 scripts" would be
|
|
107
|
+
-- wrong when it is three changes to one file.
|
|
108
|
+
local seen: { [string]: boolean } = {}
|
|
109
|
+
local distinct = 0
|
|
110
|
+
for _, edit in (edits or {}) :: { { [string]: any } } do
|
|
111
|
+
local name = leaf(edit.path)
|
|
112
|
+
if name ~= nil and not seen[name] then
|
|
113
|
+
seen[name] = true
|
|
114
|
+
distinct += 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
if distinct == 1 then
|
|
118
|
+
for name in seen do
|
|
119
|
+
return string.format("Edit %s (%s)", name, plural(total, "change"))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
return string.format("Edit %s", plural(distinct, "script"))
|
|
123
|
+
end,
|
|
124
|
+
["script.create"] = function(params)
|
|
125
|
+
local scripts = params.scripts
|
|
126
|
+
if count(scripts) == 1 then
|
|
127
|
+
local one = (scripts :: { any })[1]
|
|
128
|
+
local className = if typeof(one.className) == "string" then one.className else "Script"
|
|
129
|
+
return string.format("Create %s %s", one.name or "script", className)
|
|
130
|
+
end
|
|
131
|
+
return "Create " .. plural(count(scripts), "script")
|
|
132
|
+
end,
|
|
133
|
+
["script.grep"] = function(params)
|
|
134
|
+
local pattern = if typeof(params.pattern) == "string" then params.pattern else "?"
|
|
135
|
+
return string.format('Search scripts for "%s"', pattern)
|
|
136
|
+
end,
|
|
137
|
+
|
|
138
|
+
--[[
|
|
139
|
+
Names the class, not just whatever the caller happened to call the
|
|
140
|
+
thing. "Create MainMenu in StarterGui" was true and useless -- a name is
|
|
141
|
+
whatever someone typed, and it says nothing about what actually landed
|
|
142
|
+
in the place. `className` is required by the tool itself, so it is
|
|
143
|
+
always there; the chosen name rides along in quotes when it was given,
|
|
144
|
+
the way script.grep already quotes a search term.
|
|
145
|
+
]]
|
|
146
|
+
["instances.create"] = function(params)
|
|
147
|
+
local instances = params.instances
|
|
148
|
+
local total = count(instances)
|
|
149
|
+
if total == 1 then
|
|
150
|
+
local one = (instances :: { any })[1]
|
|
151
|
+
local className = if typeof(one.className) == "string" and one.className ~= ""
|
|
152
|
+
then one.className
|
|
153
|
+
else "instance"
|
|
154
|
+
local subject = if typeof(one.name) == "string" and one.name ~= ""
|
|
155
|
+
then string.format('%s "%s"', className, one.name)
|
|
156
|
+
else className
|
|
157
|
+
local where = leaf(one.parent)
|
|
158
|
+
return if where ~= nil
|
|
159
|
+
then string.format("Create %s in %s", subject, where)
|
|
160
|
+
else "Create " .. subject
|
|
161
|
+
end
|
|
162
|
+
local where = if total > 0 then leaf((instances :: { any })[1].parent) else nil
|
|
163
|
+
return if where ~= nil
|
|
164
|
+
then string.format("Create %s in %s", plural(total, "instance"), where)
|
|
165
|
+
else "Create " .. plural(total, "instance")
|
|
166
|
+
end,
|
|
167
|
+
["instances.modify"] = function(params)
|
|
168
|
+
local targets = params.targets
|
|
169
|
+
-- Each target carries its own path list, so the honest count is the
|
|
170
|
+
-- total across them rather than the number of entries.
|
|
171
|
+
local total = 0
|
|
172
|
+
local onlyName: string? = nil
|
|
173
|
+
--[[
|
|
174
|
+
Which properties/attributes actually changed, not just how many
|
|
175
|
+
instances did. "Modify Sword" said something happened and nothing
|
|
176
|
+
about what; a caller watching for "did the color change" had no way
|
|
177
|
+
to tell this apart from a rename or an attribute flip.
|
|
178
|
+
]]
|
|
179
|
+
local fields: { string } = {}
|
|
180
|
+
local seenFields: { [string]: boolean } = {}
|
|
181
|
+
local function noteFields(bag: any)
|
|
182
|
+
if typeof(bag) ~= "table" then
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
for key in bag :: { [string]: any } do
|
|
186
|
+
if not seenFields[key] then
|
|
187
|
+
seenFields[key] = true
|
|
188
|
+
table.insert(fields, key)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
for _, target in (targets or {}) :: { { [string]: any } } do
|
|
193
|
+
local paths = count(target.paths)
|
|
194
|
+
total += paths
|
|
195
|
+
if paths >= 1 and onlyName == nil then
|
|
196
|
+
onlyName = leaf((target.paths :: { any })[1])
|
|
197
|
+
end
|
|
198
|
+
noteFields(target.properties)
|
|
199
|
+
noteFields(target.attributes)
|
|
200
|
+
end
|
|
201
|
+
if total == 1 and onlyName ~= nil then
|
|
202
|
+
if #fields == 1 then
|
|
203
|
+
return string.format("Set %s on %s", fields[1], onlyName)
|
|
204
|
+
elseif #fields > 1 then
|
|
205
|
+
return string.format("Modify %s (%s)", onlyName, table.concat(fields, ", "))
|
|
206
|
+
end
|
|
207
|
+
return "Modify " .. onlyName
|
|
208
|
+
end
|
|
209
|
+
if #fields == 1 then
|
|
210
|
+
return string.format("Set %s on %s", fields[1], plural(total, "instance"))
|
|
211
|
+
end
|
|
212
|
+
return "Modify " .. plural(total, "instance")
|
|
213
|
+
end,
|
|
214
|
+
["instances.delete"] = function(params)
|
|
215
|
+
return "Delete " .. subjectOf(params.paths, "instance")
|
|
216
|
+
end,
|
|
217
|
+
["instances.move"] = function(params)
|
|
218
|
+
local items = params.items
|
|
219
|
+
local total = count(items)
|
|
220
|
+
local cloning = total > 0 and (items :: { any })[1].mode == "clone"
|
|
221
|
+
local verb = if cloning then "Clone" else "Move"
|
|
222
|
+
if total == 1 then
|
|
223
|
+
local one = (items :: { any })[1]
|
|
224
|
+
local what = leaf(one.path)
|
|
225
|
+
local where = leaf(one.to)
|
|
226
|
+
-- A rename in the same call changes what actually ends up in the
|
|
227
|
+
-- place; "Clone Sword to Backpack" while it lands as "GoldSword"
|
|
228
|
+
-- described an instance that was never created under that name.
|
|
229
|
+
local rename = if typeof(one.name) == "string" and one.name ~= "" then one.name else nil
|
|
230
|
+
if what ~= nil and where ~= nil then
|
|
231
|
+
local base = string.format("%s %s to %s", verb, what, where)
|
|
232
|
+
return if rename ~= nil then base .. " as " .. rename else base
|
|
233
|
+
end
|
|
234
|
+
if what ~= nil then
|
|
235
|
+
return if rename ~= nil
|
|
236
|
+
then string.format("%s %s as %s", verb, what, rename)
|
|
237
|
+
else verb .. " " .. what
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
return string.format("%s %s", verb, plural(total, "instance"))
|
|
241
|
+
end,
|
|
242
|
+
|
|
243
|
+
["discover.tree"] = function(params)
|
|
244
|
+
local where = leaf(params.path)
|
|
245
|
+
return if where ~= nil then "Browse " .. where else "Browse the place"
|
|
246
|
+
end,
|
|
247
|
+
["discover.inspect"] = function(params)
|
|
248
|
+
--[[
|
|
249
|
+
`inspect` (no explicit `properties`, not concise detail) and `modify`
|
|
250
|
+
(when setting properties) both probe a class before doing the thing
|
|
251
|
+
the caller actually asked for -- reading the properties that matter
|
|
252
|
+
for it, or typing the values it is about to write. That probe is a
|
|
253
|
+
second, genuine call to this same op, and unmarked it logged
|
|
254
|
+
identically to the read it precedes: two lines reading "Inspect X"
|
|
255
|
+
back to back, indistinguishable from asking twice by mistake.
|
|
256
|
+
]]
|
|
257
|
+
if params.probeOnly == true then
|
|
258
|
+
return "Check the class of " .. subjectOf(params.paths, "instance")
|
|
259
|
+
end
|
|
260
|
+
return "Inspect " .. subjectOf(params.paths, "instance")
|
|
261
|
+
end,
|
|
262
|
+
["discover.find"] = function(params)
|
|
263
|
+
if typeof(params.tag) == "string" and params.tag ~= "" then
|
|
264
|
+
return string.format('Find things tagged "%s"', params.tag)
|
|
265
|
+
end
|
|
266
|
+
if typeof(params.nameContains) == "string" and params.nameContains ~= "" then
|
|
267
|
+
return string.format('Find "%s"', params.nameContains)
|
|
268
|
+
end
|
|
269
|
+
if typeof(params.className) == "string" and params.className ~= "" then
|
|
270
|
+
return string.format("Find every %s", params.className)
|
|
271
|
+
end
|
|
272
|
+
return "Search the place"
|
|
273
|
+
end,
|
|
274
|
+
|
|
275
|
+
["debug.set"] = function(params)
|
|
276
|
+
local breakpoints = params.breakpoints
|
|
277
|
+
local total = count(breakpoints)
|
|
278
|
+
if total == 1 then
|
|
279
|
+
local one = (breakpoints :: { any })[1]
|
|
280
|
+
local name = leaf(one.path)
|
|
281
|
+
if name ~= nil then
|
|
282
|
+
return string.format("Break on %s:%s", name, tostring(one.line))
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
return "Set " .. plural(total, "breakpoint")
|
|
286
|
+
end,
|
|
287
|
+
["debug.clear"] = function(params)
|
|
288
|
+
local name = leaf(params.path)
|
|
289
|
+
return if name ~= nil then "Clear breakpoints in " .. name else "Clear all breakpoints"
|
|
290
|
+
end,
|
|
291
|
+
["debug.snapshots"] = function()
|
|
292
|
+
return "Read what the breakpoints caught"
|
|
293
|
+
end,
|
|
294
|
+
["debug.exceptions"] = function(params)
|
|
295
|
+
return string.format("Break on errors: %s", tostring(params.mode or "Unhandled"))
|
|
296
|
+
end,
|
|
297
|
+
|
|
298
|
+
["perf.console"] = function(params)
|
|
299
|
+
if typeof(params.level) == "string" and params.level ~= "" then
|
|
300
|
+
return string.format("Read the %s log", params.level)
|
|
301
|
+
end
|
|
302
|
+
if typeof(params.pattern) == "string" and params.pattern ~= "" then
|
|
303
|
+
return string.format('Search output for "%s"', params.pattern)
|
|
304
|
+
end
|
|
305
|
+
return "Read the output log"
|
|
306
|
+
end,
|
|
307
|
+
["perf.snapshot"] = function()
|
|
308
|
+
return "Measure performance"
|
|
309
|
+
end,
|
|
310
|
+
["perf.profile"] = function(params)
|
|
311
|
+
return string.format("Profile scripts for %ss", tostring(params.seconds or 5))
|
|
312
|
+
end,
|
|
313
|
+
["perf.coverage"] = function(params)
|
|
314
|
+
local total = count(params.enable)
|
|
315
|
+
if total > 0 then
|
|
316
|
+
return "Instrument " .. plural(total, "script")
|
|
317
|
+
end
|
|
318
|
+
return "Read line coverage"
|
|
319
|
+
end,
|
|
320
|
+
|
|
321
|
+
["playtest.control"] = function(params)
|
|
322
|
+
local op = tostring(params.op or "state")
|
|
323
|
+
--[[
|
|
324
|
+
`stop` ends a test by polling this from the *surviving* session every
|
|
325
|
+
400ms until the teardown settles, sometimes five or six times in two
|
|
326
|
+
seconds. Labelled "Check the playtest" like an agent's own status
|
|
327
|
+
check, that read as unexplained repetition with no `stop` line anywhere
|
|
328
|
+
to explain it -- the actual stop went to the session that is by then
|
|
329
|
+
gone, and its console went with it. This name is why: waiting for the
|
|
330
|
+
teardown it just started, not asking again for no reason.
|
|
331
|
+
]]
|
|
332
|
+
if op == "state" and params.waitingForStop == true then
|
|
333
|
+
return "Wait for the playtest to stop"
|
|
334
|
+
end
|
|
335
|
+
if op == "play" then
|
|
336
|
+
return "Start a playtest"
|
|
337
|
+
elseif op == "run" then
|
|
338
|
+
return "Run the place"
|
|
339
|
+
elseif op == "multiplayer" then
|
|
340
|
+
return string.format("Start a %s test", plural(tonumber(params.players) or 2, "player"))
|
|
341
|
+
elseif op == "stop" then
|
|
342
|
+
return "Stop the playtest"
|
|
343
|
+
elseif op == "endTest" then
|
|
344
|
+
return "End the test"
|
|
345
|
+
end
|
|
346
|
+
return "Check the playtest"
|
|
347
|
+
end,
|
|
348
|
+
|
|
349
|
+
["exec.run"] = function(params)
|
|
350
|
+
local source = if typeof(params.source) == "string" then params.source else ""
|
|
351
|
+
|
|
352
|
+
--[[
|
|
353
|
+
Says what the code is about, not what its first line says.
|
|
354
|
+
|
|
355
|
+
This used to show the opening line verbatim, which produced entries
|
|
356
|
+
like "Run: local GenerationService = game:GetServi..." -- a fragment
|
|
357
|
+
of a variable declaration, cut mid-word, telling the reader nothing
|
|
358
|
+
except that some code ran. The declaration is never the point of a
|
|
359
|
+
snippet; it is the setup before it.
|
|
360
|
+
|
|
361
|
+
So the services it touches stand in for it. That is the one thing a
|
|
362
|
+
snippet reliably says about itself, it is short, and it is what
|
|
363
|
+
someone glancing at the console actually wants: which part of Studio
|
|
364
|
+
is being poked.
|
|
365
|
+
]]
|
|
366
|
+
local services: { string } = {}
|
|
367
|
+
local seen: { [string]: boolean } = {}
|
|
368
|
+
for name in string.gmatch(source, 'GetService%("([%w_]+)"%)') do
|
|
369
|
+
if not seen[name] then
|
|
370
|
+
seen[name] = true
|
|
371
|
+
table.insert(services, name)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
local lines = 0
|
|
376
|
+
for _ in string.gmatch(source, "[^\r\n]+") do
|
|
377
|
+
lines += 1
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
if #services == 1 then
|
|
381
|
+
return "Run Luau on " .. services[1]
|
|
382
|
+
elseif #services == 2 then
|
|
383
|
+
return string.format("Run Luau on %s and %s", services[1], services[2])
|
|
384
|
+
elseif #services > 2 then
|
|
385
|
+
return string.format("Run Luau on %s and %d more", services[1], #services - 1)
|
|
386
|
+
end
|
|
387
|
+
return if lines <= 1 then "Run Luau" else string.format("Run Luau (%d lines)", lines)
|
|
388
|
+
end,
|
|
389
|
+
|
|
390
|
+
["viewport.select"] = function(params)
|
|
391
|
+
local total = count(params.paths)
|
|
392
|
+
if total == 0 then
|
|
393
|
+
return "Clear the selection"
|
|
394
|
+
end
|
|
395
|
+
return "Select " .. subjectOf(params.paths, "instance")
|
|
396
|
+
end,
|
|
397
|
+
["viewport.raycast"] = function()
|
|
398
|
+
return "Cast a ray"
|
|
399
|
+
end,
|
|
400
|
+
|
|
401
|
+
["capture.screenshot"] = function()
|
|
402
|
+
return "Take a screenshot"
|
|
403
|
+
end,
|
|
404
|
+
["capture.playtestId"] = function()
|
|
405
|
+
return "Capture the playtest view"
|
|
406
|
+
end,
|
|
407
|
+
["capture.decode"] = function()
|
|
408
|
+
return "Read back the capture"
|
|
409
|
+
end,
|
|
410
|
+
|
|
411
|
+
["viewport.focus"] = function(params)
|
|
412
|
+
local name = leaf(params.path)
|
|
413
|
+
return if name ~= nil then "Look at " .. name else "Move the camera"
|
|
414
|
+
end,
|
|
415
|
+
["viewport.camera"] = function()
|
|
416
|
+
return "Move the camera"
|
|
417
|
+
end,
|
|
418
|
+
|
|
419
|
+
["geometry.combine"] = function(params)
|
|
420
|
+
local op = tostring(params.op or "union")
|
|
421
|
+
local name = leaf(params.path)
|
|
422
|
+
local verb = if op == "subtract" then "Cut" elseif op == "intersect" then "Intersect" else "Merge"
|
|
423
|
+
if op == "subtract" and name ~= nil then
|
|
424
|
+
local from = count(params.with)
|
|
425
|
+
return string.format("Cut %s out of %s", plural(from, "shape"), name)
|
|
426
|
+
end
|
|
427
|
+
return if name ~= nil then string.format("%s %s", verb, name) else verb .. " parts"
|
|
428
|
+
end,
|
|
429
|
+
["geometry.fragment"] = function(params)
|
|
430
|
+
local name = leaf(params.path)
|
|
431
|
+
return if name ~= nil
|
|
432
|
+
then string.format("Shatter %s into %s", name, plural(tonumber(params.pieces) or 8, "piece"))
|
|
433
|
+
else "Shatter a part"
|
|
434
|
+
end,
|
|
435
|
+
|
|
436
|
+
["assets.insert"] = function(params)
|
|
437
|
+
return string.format("Insert asset %s", tostring(params.assetId or "?"))
|
|
438
|
+
end,
|
|
439
|
+
|
|
440
|
+
["world.history"] = function(params)
|
|
441
|
+
local action = tostring(params.action or "status")
|
|
442
|
+
if action == "status" then
|
|
443
|
+
return "Check the undo history"
|
|
444
|
+
end
|
|
445
|
+
local steps = tonumber(params.steps) or 1
|
|
446
|
+
return string.format("%s %s", if action == "undo" then "Undo" else "Redo", plural(steps, "step"))
|
|
447
|
+
end,
|
|
448
|
+
["world.collision"] = function(params)
|
|
449
|
+
local action = tostring(params.action or "list")
|
|
450
|
+
local group = if typeof(params.group) == "string" then params.group else "?"
|
|
451
|
+
if action == "list" then
|
|
452
|
+
return "List collision groups"
|
|
453
|
+
elseif action == "create" then
|
|
454
|
+
return "Create collision group " .. group
|
|
455
|
+
elseif action == "assign" then
|
|
456
|
+
return string.format("Put %s in %s", plural(count(params.paths), "instance"), group)
|
|
457
|
+
elseif action == "remove" then
|
|
458
|
+
return "Remove collision group " .. group
|
|
459
|
+
end
|
|
460
|
+
return string.format("Set %s against %s", group, tostring(params.with or "?"))
|
|
461
|
+
end,
|
|
462
|
+
|
|
463
|
+
["geometry.mirror"] = function(params)
|
|
464
|
+
local axis = string.upper(tostring(params.axis or "X"))
|
|
465
|
+
local what = plural(count(params.paths), "instance")
|
|
466
|
+
if params.copy == false then
|
|
467
|
+
return string.format("Flip %s across %s", what, axis)
|
|
468
|
+
end
|
|
469
|
+
return string.format("Mirror %s across %s", what, axis)
|
|
470
|
+
end,
|
|
471
|
+
|
|
472
|
+
["spatial.cast"] = function(params)
|
|
473
|
+
local shape = tostring(params.shape or "ray")
|
|
474
|
+
local target = if params.to ~= nil then tostring(params.to) else nil
|
|
475
|
+
if target ~= nil then
|
|
476
|
+
return string.format("Cast a %s at %s", shape, target)
|
|
477
|
+
end
|
|
478
|
+
return string.format("Cast a %s from %s", shape, tostring(params.from or "?"))
|
|
479
|
+
end,
|
|
480
|
+
["spatial.overlap"] = function(params)
|
|
481
|
+
local region = tostring(params.region or "box")
|
|
482
|
+
if region == "part" then
|
|
483
|
+
local name = leaf(params.path)
|
|
484
|
+
return if name ~= nil then "Find what overlaps " .. name else "Find overlapping parts"
|
|
485
|
+
end
|
|
486
|
+
return string.format("Find parts in a %s at %s", region, tostring(params.at or "?"))
|
|
487
|
+
end,
|
|
488
|
+
|
|
489
|
+
["audio.wire"] = function(params)
|
|
490
|
+
return string.format("Wire %s to %s", leaf(params.from) or "?", leaf(params.to) or "?")
|
|
491
|
+
end,
|
|
492
|
+
["audio.graph"] = function(params)
|
|
493
|
+
local kind = tostring(params.kind or "world")
|
|
494
|
+
local where = leaf(params.parent)
|
|
495
|
+
if kind == "ui" then
|
|
496
|
+
return "Build a UI sound"
|
|
497
|
+
end
|
|
498
|
+
return if where ~= nil then "Build a sound on " .. where else "Build a world sound"
|
|
499
|
+
end,
|
|
500
|
+
["audio.inspect"] = function(params)
|
|
501
|
+
local name = leaf(params.path)
|
|
502
|
+
return if name ~= nil then "Read the audio graph in " .. name else "Read the audio graph"
|
|
503
|
+
end,
|
|
504
|
+
|
|
505
|
+
["character.moveTo"] = function(params)
|
|
506
|
+
local name = leaf(params.path)
|
|
507
|
+
return if name ~= nil then "Walk to " .. name else string.format("Walk to %s", tostring(params.to or "?"))
|
|
508
|
+
end,
|
|
509
|
+
["character.act"] = function(params)
|
|
510
|
+
local action = tostring(params.action or "jump")
|
|
511
|
+
return titleCase(action) .. " the character"
|
|
512
|
+
end,
|
|
513
|
+
["character.state"] = function()
|
|
514
|
+
return "Check the character"
|
|
515
|
+
end,
|
|
516
|
+
|
|
517
|
+
["studio.status"] = function()
|
|
518
|
+
return "Check Studio"
|
|
519
|
+
end,
|
|
520
|
+
["studio.ping"] = function()
|
|
521
|
+
return "Ping"
|
|
522
|
+
end,
|
|
523
|
+
["input.send"] = function(params)
|
|
524
|
+
local steps = params.steps
|
|
525
|
+
local total = count(steps)
|
|
526
|
+
if total == 1 then
|
|
527
|
+
local step = (steps :: { any })[1]
|
|
528
|
+
local kind = tostring(step.kind or "key")
|
|
529
|
+
if kind == "key" then
|
|
530
|
+
local action = tostring(step.action or "tap")
|
|
531
|
+
local verb = if action == "press" then "Hold" elseif action == "release" then "Release" else "Press"
|
|
532
|
+
return string.format("%s %s", verb, tostring(step.key or "?"))
|
|
533
|
+
elseif kind == "click" then
|
|
534
|
+
return string.format("Click (%s, %s)", tostring(step.x or "?"), tostring(step.y or "?"))
|
|
535
|
+
elseif kind == "move" then
|
|
536
|
+
return "Move the pointer"
|
|
537
|
+
elseif kind == "text" then
|
|
538
|
+
return string.format('Type "%s"', tostring(step.text or ""))
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
return "Send " .. plural(total, "input step")
|
|
542
|
+
end,
|
|
543
|
+
|
|
544
|
+
["api.describe"] = function(params)
|
|
545
|
+
local className = params.className
|
|
546
|
+
return if typeof(className) == "string" and className ~= ""
|
|
547
|
+
then "Look up " .. className
|
|
548
|
+
else "Look up a class"
|
|
549
|
+
end,
|
|
550
|
+
["api.classes"] = function(params)
|
|
551
|
+
local needle = params.contains
|
|
552
|
+
return if typeof(needle) == "string" and needle ~= ""
|
|
553
|
+
then string.format('Search classes for "%s"', needle)
|
|
554
|
+
else "List classes"
|
|
555
|
+
end,
|
|
556
|
+
|
|
557
|
+
["perf.scene"] = function(params)
|
|
558
|
+
local section = params.section
|
|
559
|
+
return if typeof(section) == "string" and section ~= ""
|
|
560
|
+
then "Measure the " .. section
|
|
561
|
+
else "Measure the scene"
|
|
562
|
+
end,
|
|
563
|
+
|
|
564
|
+
--[[
|
|
565
|
+
Everything below was added after the first pass over this table, and each
|
|
566
|
+
one spent time showing as the fallback -- "Anim preview", "Data set",
|
|
567
|
+
"Terrain fill". That form is honest but says nothing about the subject,
|
|
568
|
+
which is the whole point of this module: the panel is watched to see WHAT
|
|
569
|
+
an agent is touching, and 26 of 72 operations were declining to say.
|
|
570
|
+
]]
|
|
571
|
+
["anim.read"] = function(params)
|
|
572
|
+
local name = leaf(params.assetId)
|
|
573
|
+
return if name ~= nil then "Read the animation " .. name else "Read an animation"
|
|
574
|
+
end,
|
|
575
|
+
["anim.build"] = function(params)
|
|
576
|
+
local frames = count(params.keyframes)
|
|
577
|
+
return string.format("Build an animation from %s", plural(frames, "keyframe"))
|
|
578
|
+
end,
|
|
579
|
+
["anim.preview"] = function(params)
|
|
580
|
+
local rig = leaf(params.rig) or "a rig"
|
|
581
|
+
if params.op == "stop" then
|
|
582
|
+
return "Clear the pose on " .. rig
|
|
583
|
+
end
|
|
584
|
+
local at = tonumber(params.at)
|
|
585
|
+
return if at ~= nil
|
|
586
|
+
then string.format("Pose %s at %.2fs", rig, at)
|
|
587
|
+
else "Pose " .. rig
|
|
588
|
+
end,
|
|
589
|
+
|
|
590
|
+
["data.list"] = function(params)
|
|
591
|
+
local store = params.store
|
|
592
|
+
if typeof(store) == "string" and store ~= "" then
|
|
593
|
+
return string.format("List the keys in %s", store)
|
|
594
|
+
end
|
|
595
|
+
return "List the data stores"
|
|
596
|
+
end,
|
|
597
|
+
["data.get"] = function(params)
|
|
598
|
+
return string.format("Read %s from %s", tostring(params.key), tostring(params.store))
|
|
599
|
+
end,
|
|
600
|
+
["data.versions"] = function(params)
|
|
601
|
+
return string.format("Read the history of %s", tostring(params.key))
|
|
602
|
+
end,
|
|
603
|
+
["data.set"] = function(params)
|
|
604
|
+
return string.format("SAVE over %s in %s", tostring(params.key), tostring(params.store))
|
|
605
|
+
end,
|
|
606
|
+
["data.remove"] = function(params)
|
|
607
|
+
return string.format("DELETE %s from %s", tostring(params.key), tostring(params.store))
|
|
608
|
+
end,
|
|
609
|
+
|
|
610
|
+
["discover.tags"] = function(params)
|
|
611
|
+
local where = leaf(params.path)
|
|
612
|
+
return if where ~= nil then "List the tags used in " .. where else "List every tag in use"
|
|
613
|
+
end,
|
|
614
|
+
|
|
615
|
+
["viewport.ui"] = function()
|
|
616
|
+
return "Check the interface for layout faults"
|
|
617
|
+
end,
|
|
618
|
+
["viewport.textbounds"] = function(params)
|
|
619
|
+
local name = leaf(params.path)
|
|
620
|
+
return if name ~= nil then "Measure the text in " .. name else "Measure some text"
|
|
621
|
+
end,
|
|
622
|
+
|
|
623
|
+
["perf.audit"] = function()
|
|
624
|
+
return "Look for references that point at nothing"
|
|
625
|
+
end,
|
|
626
|
+
|
|
627
|
+
["assets.audio"] = function(params)
|
|
628
|
+
local keyword = params.keyword
|
|
629
|
+
return if typeof(keyword) == "string" and keyword ~= ""
|
|
630
|
+
then string.format('Search audio for "%s"', keyword)
|
|
631
|
+
else "Search the audio library"
|
|
632
|
+
end,
|
|
633
|
+
["assets.peek"] = function(params)
|
|
634
|
+
return string.format("Look inside asset %s without inserting it", tostring(params.assetId))
|
|
635
|
+
end,
|
|
636
|
+
["assets.bake"] = function(params)
|
|
637
|
+
return "Bake " .. subjectOf(params.paths, "instance")
|
|
638
|
+
end,
|
|
639
|
+
|
|
640
|
+
["character.path"] = function(params)
|
|
641
|
+
local target = leaf(params.toPath)
|
|
642
|
+
return if target ~= nil then "Check the route to " .. target else "Check a route"
|
|
643
|
+
end,
|
|
644
|
+
|
|
645
|
+
["device.network"] = function(params)
|
|
646
|
+
local preset = params.preset
|
|
647
|
+
return if typeof(preset) == "string" and preset ~= ""
|
|
648
|
+
then string.format("Simulate a %s connection", preset)
|
|
649
|
+
else "Shape the network connection"
|
|
650
|
+
end,
|
|
651
|
+
|
|
652
|
+
["geometry.sweep"] = function(params)
|
|
653
|
+
local name = leaf(params.path)
|
|
654
|
+
return if name ~= nil then "Sweep the path of " .. name else "Sweep a motion volume"
|
|
655
|
+
end,
|
|
656
|
+
["geometry.mesh"] = function(params)
|
|
657
|
+
return "Read the geometry of " .. subjectOf(params.paths, "mesh")
|
|
658
|
+
end,
|
|
659
|
+
|
|
660
|
+
["generate.model"] = function(params)
|
|
661
|
+
local prompt = params.prompt
|
|
662
|
+
return if typeof(prompt) == "string" and prompt ~= ""
|
|
663
|
+
then string.format('Generate a model: "%s"', prompt)
|
|
664
|
+
else "Generate a model"
|
|
665
|
+
end,
|
|
666
|
+
["generate.segment"] = function(params)
|
|
667
|
+
local name = leaf(params.path)
|
|
668
|
+
return if name ~= nil then "Cut " .. name .. " into named parts" else "Segment a mesh"
|
|
669
|
+
end,
|
|
670
|
+
|
|
671
|
+
["script.open"] = function(params)
|
|
672
|
+
local name = leaf(params.path)
|
|
673
|
+
return if name ~= nil then "Open " .. name .. " in the editor" else "Open a script"
|
|
674
|
+
end,
|
|
675
|
+
|
|
676
|
+
["terrain.fill"] = function(params)
|
|
677
|
+
--[[
|
|
678
|
+
The material lives inside the shapes, not beside them.
|
|
679
|
+
|
|
680
|
+
Written first against a top-level `material`, which this op has never
|
|
681
|
+
taken, so it always fell through to the bare "Fill terrain" -- seen in
|
|
682
|
+
the panel for a call that laid 50 voxels of sand. The parameters a
|
|
683
|
+
describer reads have to be the ones the handler reads.
|
|
684
|
+
]]
|
|
685
|
+
local shapes = params.shapes
|
|
686
|
+
if typeof(shapes) == "table" and #(shapes :: { any }) > 0 then
|
|
687
|
+
local list = shapes :: { any }
|
|
688
|
+
local first = list[1]
|
|
689
|
+
local material = if typeof(first) == "table" then first.material else nil
|
|
690
|
+
local named = typeof(material) == "string" and material ~= ""
|
|
691
|
+
if #list == 1 then
|
|
692
|
+
return if named
|
|
693
|
+
then string.format("Fill terrain with %s", material)
|
|
694
|
+
else "Fill terrain"
|
|
695
|
+
end
|
|
696
|
+
return if named
|
|
697
|
+
then string.format("Fill %s, starting with %s", plural(#list, "terrain shape"), material)
|
|
698
|
+
else string.format("Fill %s", plural(#list, "terrain shape"))
|
|
699
|
+
end
|
|
700
|
+
return "Fill terrain"
|
|
701
|
+
end,
|
|
702
|
+
["terrain.clear"] = function()
|
|
703
|
+
return "CLEAR terrain"
|
|
704
|
+
end,
|
|
705
|
+
["terrain.replace"] = function(params)
|
|
706
|
+
return string.format(
|
|
707
|
+
"Replace %s terrain with %s",
|
|
708
|
+
tostring(params.from or "one material"),
|
|
709
|
+
tostring(params.to or "another")
|
|
710
|
+
)
|
|
711
|
+
end,
|
|
712
|
+
["terrain.stats"] = function()
|
|
713
|
+
return "Measure the terrain"
|
|
714
|
+
end,
|
|
715
|
+
|
|
716
|
+
["device.list"] = function()
|
|
717
|
+
return "List devices"
|
|
718
|
+
end,
|
|
719
|
+
["device.set"] = function(params)
|
|
720
|
+
local id = params.device
|
|
721
|
+
return if typeof(id) == "string" and id ~= "" then "Emulate " .. id else "Emulate a device"
|
|
722
|
+
end,
|
|
723
|
+
["device.stop"] = function()
|
|
724
|
+
return "Stop emulating a device"
|
|
725
|
+
end,
|
|
726
|
+
["device.state"] = function()
|
|
727
|
+
return "Check the emulated device"
|
|
728
|
+
end,
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
--[[
|
|
732
|
+
What kind of work an op is, for anything that wants to colour it.
|
|
733
|
+
|
|
734
|
+
Grouped by what it does to the place rather than by which handler owns it,
|
|
735
|
+
because that is the distinction someone watching cares about: reading is
|
|
736
|
+
safe and constant, writing changes their game, and running executes code.
|
|
737
|
+
]]
|
|
738
|
+
local KINDS: { [string]: string } = {
|
|
739
|
+
discover = "read",
|
|
740
|
+
studio = "read",
|
|
741
|
+
viewport = "read",
|
|
742
|
+
capture = "read",
|
|
743
|
+
instances = "write",
|
|
744
|
+
geometry = "write",
|
|
745
|
+
assets = "write",
|
|
746
|
+
world = "write",
|
|
747
|
+
exec = "run",
|
|
748
|
+
playtest = "run",
|
|
749
|
+
character = "run",
|
|
750
|
+
-- Live keys and clicks into a running playtest are an action, not a read,
|
|
751
|
+
-- and belong beside character/exec rather than defaulting to "read".
|
|
752
|
+
input = "run",
|
|
753
|
+
debug = "debug",
|
|
754
|
+
perf = "debug",
|
|
755
|
+
api = "read",
|
|
756
|
+
device = "read",
|
|
757
|
+
-- Casts and overlap tests only look; nothing in the group writes.
|
|
758
|
+
spatial = "read",
|
|
759
|
+
-- Wiring and graph building write; `audio.inspect` is carved out below.
|
|
760
|
+
audio = "write",
|
|
761
|
+
--[[
|
|
762
|
+
Absent groups defaulted to "read", which is the wrong way round for a
|
|
763
|
+
default: a missing entry made `terrain.clear` and `data.set` -- one of
|
|
764
|
+
which erases the map and the other of which overwrites a player's save
|
|
765
|
+
with no undo -- show in the panel with the same weight as an inspect.
|
|
766
|
+
A kind is a warning, so the ones that change things are named here
|
|
767
|
+
explicitly.
|
|
768
|
+
]]
|
|
769
|
+
terrain = "write",
|
|
770
|
+
generate = "write",
|
|
771
|
+
anim = "write",
|
|
772
|
+
data = "write",
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function Phrase.kindOf(op: string): string
|
|
776
|
+
local group = string.match(op, "^([^%.]+)%.") or op
|
|
777
|
+
-- Reads within the script group, writes outside it: a script edit changes
|
|
778
|
+
-- the game, a read or a grep does not, and they are far apart in weight.
|
|
779
|
+
if group == "script" then
|
|
780
|
+
local action = string.match(op, "%.(.+)$") or ""
|
|
781
|
+
return if action == "read" or action == "grep" then "read" else "write"
|
|
782
|
+
end
|
|
783
|
+
-- Same shape as the script group: one read sitting among writes.
|
|
784
|
+
if op == "audio.inspect" then
|
|
785
|
+
return "read"
|
|
786
|
+
end
|
|
787
|
+
return KINDS[group] or "read"
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
--[[
|
|
791
|
+
The readable form of one command.
|
|
792
|
+
|
|
793
|
+
Never throws and never yields: this runs on the path that logs every call,
|
|
794
|
+
including the ones that are about to fail, and a console that errors while
|
|
795
|
+
describing an error would take the diagnosis with it.
|
|
796
|
+
]]
|
|
797
|
+
function Phrase.of(op: string, params: { [string]: any }?): string
|
|
798
|
+
local describer = DESCRIBERS[op]
|
|
799
|
+
if describer ~= nil then
|
|
800
|
+
local ok, described = pcall(describer, params or {})
|
|
801
|
+
if ok and typeof(described) == "string" and described ~= "" then
|
|
802
|
+
return described
|
|
803
|
+
end
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
-- Unknown op, or a describer that met a shape it did not expect. Tidy the
|
|
807
|
+
-- wire name rather than inventing a subject for it: "instances.create"
|
|
808
|
+
-- becomes "Instances create", which is honest about knowing no better.
|
|
809
|
+
local group, action = string.match(op, "^([^%.]+)%.(.+)$")
|
|
810
|
+
if group ~= nil and action ~= nil then
|
|
811
|
+
return titleCase(group) .. " " .. action
|
|
812
|
+
end
|
|
813
|
+
return titleCase(op)
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
return Phrase
|