@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
|
@@ -1,161 +1,168 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
-- Shared bounded execution results in the VM that calls run.
|
|
3
|
-
local LogService = game:GetService("LogService")
|
|
4
|
-
local
|
|
5
|
-
|
|
6
|
-
--
|
|
7
|
-
|
|
8
|
-
local
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
|
|
14
|
-
local
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
local
|
|
19
|
-
local
|
|
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
|
-
local
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
Runtime
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
--
|
|
83
|
-
--
|
|
84
|
-
|
|
85
|
-
local
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return
|
|
1
|
+
--!strict
|
|
2
|
+
-- Shared bounded execution results in the VM that calls run.
|
|
3
|
+
local LogService = game:GetService("LogService")
|
|
4
|
+
local LogBuffer = require(script.Parent.LogBuffer)
|
|
5
|
+
local Serialize = require(script.Parent.Serialize)
|
|
6
|
+
-- A chunk that prints in a loop would otherwise return a response no context
|
|
7
|
+
-- window can hold.
|
|
8
|
+
local MAX_OUTPUT_LINES = 200
|
|
9
|
+
local MAX_RETURN_VALUES = 10
|
|
10
|
+
|
|
11
|
+
-- Returned tables are walked rather than counted, but not without limit: a
|
|
12
|
+
-- chunk that returns a whole config tree, or one with a cycle in it, must not be
|
|
13
|
+
-- able to produce a response nothing can read.
|
|
14
|
+
local MAX_TABLE_ENTRIES = 50
|
|
15
|
+
local MAX_TABLE_DEPTH = 4
|
|
16
|
+
|
|
17
|
+
local function describe(value: any, depth: number?, seen: { [any]: boolean }?): any
|
|
18
|
+
local level = depth or 0
|
|
19
|
+
local visited = seen or {}
|
|
20
|
+
local kind = typeof(value)
|
|
21
|
+
|
|
22
|
+
if kind == "function" or kind == "thread" then
|
|
23
|
+
return string.format("<%s>", kind)
|
|
24
|
+
end
|
|
25
|
+
if kind ~= "table" then
|
|
26
|
+
return Serialize.value(value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
local source = value :: { [any]: any }
|
|
30
|
+
-- Named separately from the depth cap, because they mean opposite things to
|
|
31
|
+
-- a reader. The depth cap says "there is more below this"; a cycle says
|
|
32
|
+
-- "this is the table you are already inside". Reporting the second as the
|
|
33
|
+
-- first printed `t.self.self.self` as three distinct nested tables, which
|
|
34
|
+
-- reads as real structure that does not exist.
|
|
35
|
+
if visited[source] then
|
|
36
|
+
return "<circular reference>"
|
|
37
|
+
end
|
|
38
|
+
if level >= MAX_TABLE_DEPTH then
|
|
39
|
+
return "<nested table>"
|
|
40
|
+
end
|
|
41
|
+
visited[source] = true
|
|
42
|
+
|
|
43
|
+
local count = 0
|
|
44
|
+
for _ in source do
|
|
45
|
+
count += 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
-- Arrays keep their shape so they read as lists on the other side; anything
|
|
49
|
+
-- with non-sequential keys becomes a map with stringified keys.
|
|
50
|
+
if count == #source then
|
|
51
|
+
local list: { any } = {}
|
|
52
|
+
for index, item in ipairs(source) do
|
|
53
|
+
if index > MAX_TABLE_ENTRIES then
|
|
54
|
+
table.insert(list, string.format("<%d more>", count - MAX_TABLE_ENTRIES))
|
|
55
|
+
break
|
|
56
|
+
end
|
|
57
|
+
table.insert(list, describe(item, level + 1, visited))
|
|
58
|
+
end
|
|
59
|
+
-- Cleared on the way out so the same table appearing twice side by side
|
|
60
|
+
-- still renders twice; only a table containing itself is a cycle.
|
|
61
|
+
visited[source] = nil
|
|
62
|
+
return list
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local map: { [string]: any } = {}
|
|
66
|
+
local shown = 0
|
|
67
|
+
for key, item in source do
|
|
68
|
+
shown += 1
|
|
69
|
+
if shown > MAX_TABLE_ENTRIES then
|
|
70
|
+
map["..."] = string.format("<%d more>", count - MAX_TABLE_ENTRIES)
|
|
71
|
+
break
|
|
72
|
+
end
|
|
73
|
+
map[tostring(key)] = describe(item, level + 1, visited)
|
|
74
|
+
end
|
|
75
|
+
visited[source] = nil
|
|
76
|
+
return map
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
local Runtime = {}
|
|
80
|
+
Runtime.describe = describe
|
|
81
|
+
function Runtime.run(chunk: () -> ...any, timeout: number?): { [string]: any }
|
|
82
|
+
-- Captured rather than inferred: a chunk's prints are usually the point, and
|
|
83
|
+
-- reading them back from the log afterwards would also pick up whatever else
|
|
84
|
+
-- the place logged in the meantime.
|
|
85
|
+
local output: { { [string]: any } } = {}
|
|
86
|
+
local connection = LogService.MessageOut:Connect(function(message, messageType, context)
|
|
87
|
+
if #output < MAX_OUTPUT_LINES then
|
|
88
|
+
-- A structured log's context rides on the line, as it does in `console`.
|
|
89
|
+
local encoded = LogBuffer.encodeContext(context)
|
|
90
|
+
if encoded then
|
|
91
|
+
message ..= " " .. encoded
|
|
92
|
+
end
|
|
93
|
+
table.insert(output, {
|
|
94
|
+
message = message,
|
|
95
|
+
level = if messageType == Enum.MessageType.MessageError
|
|
96
|
+
then "error"
|
|
97
|
+
elseif messageType == Enum.MessageType.MessageWarning then "warning"
|
|
98
|
+
elseif messageType == Enum.MessageType.MessageInfo then "info"
|
|
99
|
+
else "print",
|
|
100
|
+
})
|
|
101
|
+
end
|
|
102
|
+
end)
|
|
103
|
+
|
|
104
|
+
local started = os.clock()
|
|
105
|
+
local results: any = nil
|
|
106
|
+
if timeout then
|
|
107
|
+
local worker = task.spawn(function() results = table.pack(pcall(chunk :: any)) end)
|
|
108
|
+
local deadline = started + timeout
|
|
109
|
+
while results == nil and os.clock() < deadline do task.wait() end
|
|
110
|
+
if results == nil then
|
|
111
|
+
task.cancel(worker)
|
|
112
|
+
results = table.pack(false, "CLIENT_TIMEOUT: execution exceeded " .. tostring(timeout) .. "s")
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
results = table.pack(pcall(chunk :: () -> ...any))
|
|
116
|
+
end
|
|
117
|
+
local elapsed = os.clock() - started
|
|
118
|
+
|
|
119
|
+
--[[
|
|
120
|
+
MessageOut is deferred: `print` returns before the event fires, so
|
|
121
|
+
disconnecting straight after the chunk finished captured nothing at all.
|
|
122
|
+
A short settle picks the lines up.
|
|
123
|
+
|
|
124
|
+
It is bounded and stops as soon as the flow stops, rather than waiting a
|
|
125
|
+
fixed period, so a chunk that logged nothing costs almost no time and one
|
|
126
|
+
that logged plenty is not truncated.
|
|
127
|
+
]]
|
|
128
|
+
local settleDeadline = os.clock() + 0.5
|
|
129
|
+
local seen = #output
|
|
130
|
+
local quietFrames = 0
|
|
131
|
+
while os.clock() < settleDeadline and quietFrames < 3 do
|
|
132
|
+
task.wait()
|
|
133
|
+
if #output == seen then
|
|
134
|
+
quietFrames += 1
|
|
135
|
+
else
|
|
136
|
+
seen = #output
|
|
137
|
+
quietFrames = 0
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
connection:Disconnect()
|
|
142
|
+
|
|
143
|
+
if not results[1] then
|
|
144
|
+
return {
|
|
145
|
+
ok = false,
|
|
146
|
+
error = tostring(results[2]),
|
|
147
|
+
output = output,
|
|
148
|
+
milliseconds = math.floor(elapsed * 1000 + 0.5),
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
local returned: { any } = {}
|
|
153
|
+
for index = 2, math.min(results.n, MAX_RETURN_VALUES + 1) do
|
|
154
|
+
-- A nil return is written out rather than dropped. `table.insert` of nil
|
|
155
|
+
-- does nothing, so `return a, nil, b` came back as two values and every
|
|
156
|
+
-- one after the nil shifted into the wrong position.
|
|
157
|
+
local value = results[index]
|
|
158
|
+
table.insert(returned, if value == nil then "nil" else describe(value))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
ok = true,
|
|
163
|
+
returned = returned,
|
|
164
|
+
output = output,
|
|
165
|
+
milliseconds = math.floor(elapsed * 1000 + 0.5),
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
return Runtime
|
|
@@ -35,6 +35,10 @@ local SLACK = 500
|
|
|
35
35
|
-- handful of entries is a mismatch rather than a late delivery.
|
|
36
36
|
local CORRELATION_WINDOW = 20
|
|
37
37
|
|
|
38
|
+
-- A structured log's context, as JSON, is cut here: Roblox's own server logs
|
|
39
|
+
-- drop context past 1KiB, so nothing longer is worth carrying.
|
|
40
|
+
local MAX_CONTEXT = 1000
|
|
41
|
+
|
|
38
42
|
export type Entry = {
|
|
39
43
|
sequence: number?,
|
|
40
44
|
level: string,
|
|
@@ -43,6 +47,8 @@ export type Entry = {
|
|
|
43
47
|
-- Present on errors once the matching stack trace has been correlated.
|
|
44
48
|
stack: string?,
|
|
45
49
|
source: string?,
|
|
50
|
+
-- The table a structured log (`LogService:Info(msg, context)`) carried, as JSON.
|
|
51
|
+
context: string?,
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
local LEVELS: { [number]: string } = {
|
|
@@ -107,6 +113,13 @@ end
|
|
|
107
113
|
-- Each player gets a separate stream and token. Rejoining, reconnecting to a
|
|
108
114
|
-- different server DataModel, or a plugin reload invalidates old cursors.
|
|
109
115
|
function LogBuffer.startClient(player: Player)
|
|
116
|
+
-- A relay reinstalled after a respawn is the same player in the same session:
|
|
117
|
+
-- keep its log and cursor. Only a player that left (stopClient) starts fresh.
|
|
118
|
+
local existing = clientBuffers[player]
|
|
119
|
+
if existing then
|
|
120
|
+
existing.ready = false
|
|
121
|
+
return
|
|
122
|
+
end
|
|
110
123
|
clientBuffers[player] = { token = HttpService:GenerateGUID(false), entries = {}, dropped = 0, sequence = 0, startedAt = os.time(), ready = false, orphan = {} }
|
|
111
124
|
end
|
|
112
125
|
|
|
@@ -146,6 +159,7 @@ function LogBuffer.pushClient(player: Player, payload: any)
|
|
|
146
159
|
timestamp = if typeof(payload.timestamp) == "number" and payload.timestamp >= 0 and payload.timestamp < 100000000000 then payload.timestamp else os.time(),
|
|
147
160
|
stack = if typeof(payload.stack) == "string" then string.sub(payload.stack, 1, 4000) else nil,
|
|
148
161
|
source = if typeof(payload.source) == "string" then string.sub(payload.source, 1, 500) else nil,
|
|
162
|
+
context = if typeof(payload.context) == "string" then string.sub(payload.context, 1, MAX_CONTEXT) else nil,
|
|
149
163
|
}
|
|
150
164
|
local waiting = buffer.orphan[entry.message]
|
|
151
165
|
if level == "error" and waiting then
|
|
@@ -176,12 +190,60 @@ function LogBuffer.stream(player: Player?): ({ Entry }, number, string, number,
|
|
|
176
190
|
return table.move(entries, 1, #entries, 1, {}), dropped, studioToken, nextSequence, LogBuffer.recordingSeconds()
|
|
177
191
|
end
|
|
178
192
|
|
|
179
|
-
|
|
193
|
+
--[[
|
|
194
|
+
The context table of a structured log, as compact JSON.
|
|
195
|
+
|
|
196
|
+
`LogService:Info("{player} leveled up", { player = "Bob" })` prints only the
|
|
197
|
+
filled-in message; the table is the part that says which player and what
|
|
198
|
+
state, and MessageOut hands it over as a third argument. Plain prints pass an
|
|
199
|
+
empty table, which is dropped. Capped at 1000 characters -- the ceiling
|
|
200
|
+
Roblox's own server logs apply -- and a table JSON cannot hold (a Vector3, an
|
|
201
|
+
Instance) falls back to text rather than being lost.
|
|
202
|
+
]]
|
|
203
|
+
-- JSONEncode writes a Vector3 or an Instance as `null` without complaint, so
|
|
204
|
+
-- anything JSON has no shape for is turned into its text first.
|
|
205
|
+
local function plain(value: any, depth: number): any
|
|
206
|
+
local kind = typeof(value)
|
|
207
|
+
if kind == "string" or kind == "number" or kind == "boolean" then
|
|
208
|
+
return value
|
|
209
|
+
elseif kind == "table" then
|
|
210
|
+
if depth >= 4 then
|
|
211
|
+
return "<table>"
|
|
212
|
+
end
|
|
213
|
+
local copy = {}
|
|
214
|
+
for key, item in value do
|
|
215
|
+
copy[if typeof(key) == "number" then key else tostring(key)] = plain(item, depth + 1)
|
|
216
|
+
end
|
|
217
|
+
return copy
|
|
218
|
+
elseif kind == "Instance" then
|
|
219
|
+
local ok, full = pcall(function()
|
|
220
|
+
return value:GetFullName()
|
|
221
|
+
end)
|
|
222
|
+
return if ok then full else value.Name
|
|
223
|
+
end
|
|
224
|
+
return tostring(value)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
function LogBuffer.encodeContext(context: any): string?
|
|
228
|
+
if typeof(context) ~= "table" or next(context) == nil then
|
|
229
|
+
return nil
|
|
230
|
+
end
|
|
231
|
+
local ok, encoded = pcall(function()
|
|
232
|
+
return HttpService:JSONEncode(plain(context, 0))
|
|
233
|
+
end)
|
|
234
|
+
if not ok then
|
|
235
|
+
return nil
|
|
236
|
+
end
|
|
237
|
+
return string.sub(encoded :: string, 1, MAX_CONTEXT)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
local function push(message: string, messageType: Enum.MessageType, timestamp: number?, context: any?)
|
|
180
241
|
local level = LEVELS[messageType.Value] or "print"
|
|
181
242
|
local entry: Entry = {
|
|
182
243
|
level = level,
|
|
183
244
|
message = message,
|
|
184
245
|
timestamp = timestamp or os.time(),
|
|
246
|
+
context = LogBuffer.encodeContext(context),
|
|
185
247
|
}
|
|
186
248
|
|
|
187
249
|
-- Claim a trace that beat its own error line here.
|
|
@@ -254,7 +316,7 @@ function LogBuffer.start()
|
|
|
254
316
|
end
|
|
255
317
|
end
|
|
256
318
|
|
|
257
|
-
LogService.MessageOut:Connect(function(message: string, messageType: Enum.MessageType)
|
|
319
|
+
LogService.MessageOut:Connect(function(message: string, messageType: Enum.MessageType, context: any?)
|
|
258
320
|
local buffered = collecting
|
|
259
321
|
|
|
260
322
|
if buffered then
|
|
@@ -298,7 +360,7 @@ function LogBuffer.start()
|
|
|
298
360
|
return
|
|
299
361
|
end
|
|
300
362
|
|
|
301
|
-
push(message, messageType)
|
|
363
|
+
push(message, messageType, nil, context)
|
|
302
364
|
end)
|
|
303
365
|
|
|
304
366
|
--[[
|
package/plugin/src/Paths.luau
CHANGED
|
@@ -147,13 +147,19 @@ local function childBySegment(
|
|
|
147
147
|
|
|
148
148
|
if ordinal then
|
|
149
149
|
local list = childrenByName(parent, nil)[name]
|
|
150
|
-
if list
|
|
151
|
-
return nil, nil
|
|
152
|
-
end
|
|
153
|
-
local child = list[ordinal]
|
|
150
|
+
local child = if list then list[ordinal] else nil
|
|
154
151
|
if child then
|
|
155
152
|
return child, nil
|
|
156
153
|
end
|
|
154
|
+
-- Not an index after all: a child literally named "x[1]". Typed by hand,
|
|
155
|
+
-- or from a path emitted before `Paths.of` began marking such names.
|
|
156
|
+
local literal = parent:FindFirstChild(segment)
|
|
157
|
+
if literal then
|
|
158
|
+
return literal, nil
|
|
159
|
+
end
|
|
160
|
+
if list == nil then
|
|
161
|
+
return nil, nil
|
|
162
|
+
end
|
|
157
163
|
return nil,
|
|
158
164
|
string.format(
|
|
159
165
|
'"%s" has %d child%s named "%s", so [%d] is past the end. Indexes are '
|
|
@@ -343,6 +349,11 @@ function Paths.of(instance: Instance, memo: NameIndex?): string
|
|
|
343
349
|
while current ~= nil and current ~= game do
|
|
344
350
|
local node = current :: Instance
|
|
345
351
|
local ordinal = siblingIndex(node, memo)
|
|
352
|
+
-- A name that itself ends in "[n]" would read back as an index into a
|
|
353
|
+
-- shorter name. An explicit index keeps it literal: "x[1][1]".
|
|
354
|
+
if ordinal == nil and string.match(node.Name, "%[%d+%]$") then
|
|
355
|
+
ordinal = 1
|
|
356
|
+
end
|
|
346
357
|
table.insert(
|
|
347
358
|
segments,
|
|
348
359
|
1,
|
package/plugin/src/Phrase.luau
CHANGED
|
@@ -341,6 +341,8 @@ local DESCRIBERS: { [string]: Describer } = {
|
|
|
341
341
|
return "Run the place"
|
|
342
342
|
elseif op == "multiplayer" then
|
|
343
343
|
return string.format("Start a %s test", plural(tonumber(params.players) or 2, "player"))
|
|
344
|
+
elseif op == "addPlayers" then
|
|
345
|
+
return string.format("Add %s to the test", plural(tonumber(params.players) or 1, "player"))
|
|
344
346
|
elseif op == "stop" then
|
|
345
347
|
return "Stop the playtest"
|
|
346
348
|
elseif op == "endTest" then
|
|
@@ -566,29 +568,11 @@ local DESCRIBERS: { [string]: Describer } = {
|
|
|
566
568
|
|
|
567
569
|
--[[
|
|
568
570
|
Everything below was added after the first pass over this table, and each
|
|
569
|
-
one spent time showing as the fallback -- "
|
|
571
|
+
one spent time showing as the fallback -- "Data set",
|
|
570
572
|
"Terrain fill". That form is honest but says nothing about the subject,
|
|
571
573
|
which is the whole point of this module: the panel is watched to see WHAT
|
|
572
574
|
an agent is touching, and 26 of 72 operations were declining to say.
|
|
573
575
|
]]
|
|
574
|
-
["anim.read"] = function(params)
|
|
575
|
-
local name = leaf(params.assetId)
|
|
576
|
-
return if name ~= nil then "Read the animation " .. name else "Read an animation"
|
|
577
|
-
end,
|
|
578
|
-
["anim.build"] = function(params)
|
|
579
|
-
local frames = count(params.keyframes)
|
|
580
|
-
return string.format("Build an animation from %s", plural(frames, "keyframe"))
|
|
581
|
-
end,
|
|
582
|
-
["anim.preview"] = function(params)
|
|
583
|
-
local rig = leaf(params.rig) or "a rig"
|
|
584
|
-
if params.op == "stop" then
|
|
585
|
-
return "Clear the pose on " .. rig
|
|
586
|
-
end
|
|
587
|
-
local at = tonumber(params.at)
|
|
588
|
-
return if at ~= nil
|
|
589
|
-
then string.format("Pose %s at %.2fs", rig, at)
|
|
590
|
-
else "Pose " .. rig
|
|
591
|
-
end,
|
|
592
576
|
|
|
593
577
|
["data.list"] = function(params)
|
|
594
578
|
local store = params.store
|
|
@@ -771,7 +755,6 @@ local KINDS: { [string]: string } = {
|
|
|
771
755
|
]]
|
|
772
756
|
terrain = "write",
|
|
773
757
|
generate = "write",
|
|
774
|
-
anim = "write",
|
|
775
758
|
data = "write",
|
|
776
759
|
}
|
|
777
760
|
|
|
@@ -67,6 +67,20 @@ local function confirm(target: LuaSourceContainer, produced: string?)
|
|
|
67
67
|
return
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
--[[
|
|
71
|
+
No document, no race. The overwrite comes from an editor tab that was
|
|
72
|
+
loading while we wrote; a tab opened afterwards loads the new source. So
|
|
73
|
+
a script nobody has open is confirmed by one read, instead of paying the
|
|
74
|
+
settle -- which cost every written script 200ms, one after another, on
|
|
75
|
+
every script_edit and script_create.
|
|
76
|
+
]]
|
|
77
|
+
local found, document = pcall(function()
|
|
78
|
+
return ScriptEditorService:FindScriptDocument(target)
|
|
79
|
+
end)
|
|
80
|
+
if found and document == nil and ScriptEdit.read(target) == produced then
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
70
84
|
-- Three rounds: the race is with one load, so a single rewrite settles it,
|
|
71
85
|
-- and a script that discards three identical writes is failing for a reason
|
|
72
86
|
-- repeating will not fix.
|
|
@@ -403,6 +403,11 @@ function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
|
403
403
|
return true, Vector2.new(numbers[1], numbers[2]), nil
|
|
404
404
|
end
|
|
405
405
|
if valueType == "Color3" then
|
|
406
|
+
-- "#FF8800", the way colours are usually written everywhere else.
|
|
407
|
+
local hex = if kind == "string" then string.match(text, "^%s*#(%x%x%x%x%x%x)%s*$") else nil
|
|
408
|
+
if hex then
|
|
409
|
+
return true, Color3.fromHex(hex), nil
|
|
410
|
+
end
|
|
406
411
|
if #numbers < 3 then
|
|
407
412
|
return false, nil, 'expected three 0-1 components, e.g. "1, 0.5, 0"'
|
|
408
413
|
end
|
|
@@ -435,6 +440,18 @@ function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
|
435
440
|
return true, UDim.new(numbers[1], numbers[2]), nil
|
|
436
441
|
end
|
|
437
442
|
if valueType == "CFrame" then
|
|
443
|
+
-- Twelve numbers are a full matrix -- what `tostring(cframe)` prints. Read
|
|
444
|
+
-- as "position | rotation" they gave a silently wrong orientation.
|
|
445
|
+
if #numbers == 12 then
|
|
446
|
+
return true,
|
|
447
|
+
CFrame.new(
|
|
448
|
+
numbers[1], numbers[2], numbers[3],
|
|
449
|
+
numbers[4], numbers[5], numbers[6],
|
|
450
|
+
numbers[7], numbers[8], numbers[9],
|
|
451
|
+
numbers[10], numbers[11], numbers[12]
|
|
452
|
+
),
|
|
453
|
+
nil
|
|
454
|
+
end
|
|
438
455
|
if #numbers >= 6 then
|
|
439
456
|
return true,
|
|
440
457
|
CFrame.new(numbers[1], numbers[2], numbers[3])
|
package/plugin/src/TextEdit.luau
CHANGED
|
@@ -263,6 +263,12 @@ function TextEdit.apply(path: string, source: string, edits: { Edit }): string
|
|
|
263
263
|
end
|
|
264
264
|
|
|
265
265
|
local text = table.concat(lines, "\n")
|
|
266
|
+
-- `toLines` drops the final newline, so it is put back here. Without this
|
|
267
|
+
-- every edit -- even a find/replace in the middle -- stripped it silently,
|
|
268
|
+
-- and a `find` that included it could never match.
|
|
269
|
+
if string.sub(source, -1) == "\n" and #lines > 0 then
|
|
270
|
+
text ..= "\n"
|
|
271
|
+
end
|
|
266
272
|
|
|
267
273
|
for _, edit in findEdits do
|
|
268
274
|
local needle = edit.find :: string
|
|
@@ -122,6 +122,14 @@ local function collect(entries: { any }, className: string, wantInherited: boole
|
|
|
122
122
|
deprecated += 1
|
|
123
123
|
continue
|
|
124
124
|
end
|
|
125
|
+
-- No permits at all means no script can touch it: Part's old `shap` and
|
|
126
|
+
-- `shape` aliases, or engine-only calls like GetPhysicsCost. The engine
|
|
127
|
+
-- does not flag those as deprecated, so they are held back here too.
|
|
128
|
+
local permits = if typeof(entry) == "table" then (entry :: any).Permits else nil
|
|
129
|
+
if typeof(permits) == "table" and next(permits) == nil then
|
|
130
|
+
deprecated += 1
|
|
131
|
+
continue
|
|
132
|
+
end
|
|
125
133
|
local own = typeof(entry) ~= "table" or tostring((entry :: any).Owner) == className
|
|
126
134
|
if not own then
|
|
127
135
|
inherited += 1
|