@el4cteo/rbx-studio-mcp 0.8.3 → 0.8.6
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 +24 -6
- package/dist/bridge/rpc.js +1 -1
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/apidump.js +75 -0
- package/dist/lib/apidump.js.map +1 -1
- package/dist/lib/errors.js +5 -4
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/format.js +92 -25
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/notices.js +29 -0
- package/dist/lib/notices.js.map +1 -0
- package/dist/lib/protocol.js.map +1 -1
- package/dist/lib/sync.js +1141 -0
- package/dist/lib/sync.js.map +1 -0
- package/dist/lib/syncplan.js +338 -0
- package/dist/lib/syncplan.js.map +1 -0
- package/dist/lib/tool.js +9 -2
- package/dist/lib/tool.js.map +1 -1
- package/dist/tools/discover.js +6 -1
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +5 -0
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/instances.js +2 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +28 -3
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/screenshot.js +7 -3
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/scripts.js +110 -28
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/sync.js +169 -0
- package/dist/tools/sync.js.map +1 -0
- package/package.json +4 -4
- package/plugin/src/Commands.luau +72 -5
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Console.luau +5 -0
- package/plugin/src/Dispatch.luau +131 -90
- package/plugin/src/ExecRuntime.luau +190 -168
- package/plugin/src/LogBuffer.luau +38 -9
- package/plugin/src/Paths.luau +42 -0
- package/plugin/src/Phrase.luau +41 -0
- package/plugin/src/Prompt.luau +23 -3
- package/plugin/src/ScriptEdit.luau +94 -8
- package/plugin/src/Serialize.luau +16 -1
- package/plugin/src/Transport.luau +5 -2
- package/plugin/src/Undo.luau +74 -10
- package/plugin/src/handlers/Capture.luau +818 -809
- package/plugin/src/handlers/Debug.luau +19 -16
- package/plugin/src/handlers/Discover.luau +31 -1
- package/plugin/src/handlers/Perf.luau +100 -21
- package/plugin/src/handlers/Scripts.luau +274 -90
- package/plugin/src/handlers/Sync.luau +968 -0
- package/plugin/src/init.server.luau +47 -2
- package/scripts/build.mjs +14 -0
- package/scripts/sync-fake.mjs +191 -0
- package/scripts/test-live-sync-scale.mjs +150 -0
- package/scripts/test-live-sync.mjs +232 -0
- package/scripts/test-live-tools.mjs +6 -1
- package/scripts/test-plugin.mjs +17 -0
- package/scripts/test-results.mjs +41 -0
- package/scripts/test-sync-more.mjs +228 -0
- package/scripts/test-sync.mjs +245 -0
- package/dist/tools/spatial.js +0 -135
- package/dist/tools/spatial.js.map +0 -1
- package/dist/tools/upload.js +0 -294
- package/dist/tools/upload.js.map +0 -1
|
@@ -38,6 +38,70 @@ function ScriptEdit.read(target: LuaSourceContainer): string
|
|
|
38
38
|
return (target :: SourceContainer).Source
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
--[[
|
|
42
|
+
A short fingerprint of a script's source, for detecting that it moved.
|
|
43
|
+
|
|
44
|
+
Handed out by `script_read` and passed back to `script_edit`, and kept by
|
|
45
|
+
`sync` to tell which side of a synced script changed. `edit`, which refuses to write when
|
|
46
|
+
the live source no longer matches. That is the only thing standing between
|
|
47
|
+
two agents on one place and a silent overwrite: a line-range edit computed
|
|
48
|
+
against source somebody has since changed still applies cleanly, it just
|
|
49
|
+
applies to the wrong lines, and nothing anywhere reports it.
|
|
50
|
+
|
|
51
|
+
FNV-1a over the whole string, with the length appended. Not a security
|
|
52
|
+
hash and does not need to be -- it is guarding against ordinary concurrent
|
|
53
|
+
editing, not against someone constructing a collision. The length is there
|
|
54
|
+
because it is free and rules out the whole class of same-length accidents.
|
|
55
|
+
|
|
56
|
+
The multiply is split into 16-bit halves on purpose. `hash * 16777619` with
|
|
57
|
+
a 32-bit hash reaches 2^56, past the 2^53 where doubles stop being exact,
|
|
58
|
+
so the low bits -- the ones that carry the mixing -- would be quietly
|
|
59
|
+
rounded away.
|
|
60
|
+
]]
|
|
61
|
+
function ScriptEdit.fingerprint(source: string): string
|
|
62
|
+
local hash = 2166136261
|
|
63
|
+
local length = #source
|
|
64
|
+
local index = 1
|
|
65
|
+
while index <= length do
|
|
66
|
+
local last = math.min(index + 511, length)
|
|
67
|
+
local chunk = { string.byte(source, index, last) }
|
|
68
|
+
for _, byte in chunk do
|
|
69
|
+
hash = bit32.bxor(hash, byte)
|
|
70
|
+
local low = bit32.band(hash, 0xFFFF)
|
|
71
|
+
local high = bit32.rshift(hash, 16)
|
|
72
|
+
-- 16777619 == 0x01000193, so 0x0193 is 403 and 0x0100 is 256.
|
|
73
|
+
hash = bit32.band(low * 403 + bit32.lshift(bit32.band(high * 403 + low * 256, 0xFFFF), 16), 0xFFFFFFFF)
|
|
74
|
+
end
|
|
75
|
+
index = last + 1
|
|
76
|
+
end
|
|
77
|
+
return string.format("%08x-%x", hash, length)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
--[[
|
|
81
|
+
A script's revision, computed once per distinct text.
|
|
82
|
+
|
|
83
|
+
Fingerprinting walks every byte in Luau, and `sync` asks for the revision of
|
|
84
|
+
every script in a place on each run -- so a place with megabytes of code paid
|
|
85
|
+
for hashing all of it after every save, to learn that one file changed. The
|
|
86
|
+
text is kept beside its revision per script, and a lookup whose text is the
|
|
87
|
+
same string skips the hash: Luau interns strings, so that comparison is a
|
|
88
|
+
pointer check, not a second pass over the source.
|
|
89
|
+
|
|
90
|
+
Weak keys, so a deleted script's entry goes with it.
|
|
91
|
+
]]
|
|
92
|
+
local revisions: { [Instance]: { source: string, revision: string } } = setmetatable({}, { __mode = "k" }) :: any
|
|
93
|
+
|
|
94
|
+
function ScriptEdit.revisionOf(target: LuaSourceContainer, source: string?): string
|
|
95
|
+
local text = source or ScriptEdit.read(target)
|
|
96
|
+
local cached = revisions[target]
|
|
97
|
+
if cached ~= nil and cached.source == text then
|
|
98
|
+
return cached.revision
|
|
99
|
+
end
|
|
100
|
+
local revision = ScriptEdit.fingerprint(text)
|
|
101
|
+
revisions[target] = { source = text, revision = revision }
|
|
102
|
+
return revision
|
|
103
|
+
end
|
|
104
|
+
|
|
41
105
|
--[[
|
|
42
106
|
Makes sure a write that reported success actually stuck.
|
|
43
107
|
|
|
@@ -59,11 +123,16 @@ end
|
|
|
59
123
|
caused this is only reachable when the USER opens one in the same moment we
|
|
60
124
|
write to it -- rare, and cheap enough to cover anyway. One settle, one read,
|
|
61
125
|
and a rewrite if it did not take.
|
|
126
|
+
|
|
127
|
+
A rewrite only ever replaces `original`, the text our write replaced. That
|
|
128
|
+
is what a finishing load puts back. Anything else in the buffer is somebody
|
|
129
|
+
typing after the write landed, and rewriting it "to make ours stick" would
|
|
130
|
+
delete their work -- so that is reported instead, and their text is kept.
|
|
62
131
|
]]
|
|
63
132
|
local SETTLE = 0.2
|
|
64
133
|
|
|
65
|
-
local function confirm(target: LuaSourceContainer, produced: string?)
|
|
66
|
-
if produced == nil then
|
|
134
|
+
local function confirm(target: LuaSourceContainer, produced: string?, original: string?)
|
|
135
|
+
if produced == nil or produced == original then
|
|
67
136
|
return
|
|
68
137
|
end
|
|
69
138
|
|
|
@@ -86,20 +155,35 @@ local function confirm(target: LuaSourceContainer, produced: string?)
|
|
|
86
155
|
-- repeating will not fix.
|
|
87
156
|
for _ = 1, 3 do
|
|
88
157
|
task.wait(SETTLE)
|
|
89
|
-
|
|
158
|
+
local current = ScriptEdit.read(target)
|
|
159
|
+
if current == produced then
|
|
90
160
|
return
|
|
91
161
|
end
|
|
162
|
+
if current ~= original then
|
|
163
|
+
break
|
|
164
|
+
end
|
|
92
165
|
pcall(function()
|
|
93
|
-
ScriptEditorService:UpdateSourceAsync(target, function()
|
|
94
|
-
|
|
166
|
+
ScriptEditorService:UpdateSourceAsync(target, function(source)
|
|
167
|
+
-- Checked again at commit time: the user may have typed since the read.
|
|
168
|
+
return if source == original then produced :: string else source
|
|
95
169
|
end)
|
|
96
170
|
end)
|
|
97
171
|
end
|
|
172
|
+
|
|
173
|
+
if ScriptEdit.read(target) ~= produced then
|
|
174
|
+
Dispatch.fail(
|
|
175
|
+
"VERIFY_FAILED",
|
|
176
|
+
string.format("%s changed while the write was being confirmed.", target:GetFullName()),
|
|
177
|
+
"The newer text was kept, not overwritten. Read the script again before retrying."
|
|
178
|
+
)
|
|
179
|
+
end
|
|
98
180
|
end
|
|
99
181
|
|
|
100
182
|
--[[
|
|
101
|
-
Replaces source through the editor. `transform` receives the current text
|
|
102
|
-
|
|
183
|
+
Replaces source through the editor. `transform` receives the current text --
|
|
184
|
+
read at commit time, so it is where a "has this changed since I read it"
|
|
185
|
+
check belongs -- and returns the replacement; returning the input unchanged
|
|
186
|
+
is a no-op.
|
|
103
187
|
|
|
104
188
|
`transform` is allowed to raise a structured Dispatch failure -- a bad line
|
|
105
189
|
range is only discoverable once the authoritative source is in hand. Such a
|
|
@@ -115,10 +199,12 @@ function ScriptEdit.write(target: LuaSourceContainer, transform: (string) -> str
|
|
|
115
199
|
local pending: any = nil
|
|
116
200
|
|
|
117
201
|
local produced: string? = nil
|
|
202
|
+
local original: string? = nil
|
|
118
203
|
local ok, err = pcall(function()
|
|
119
204
|
ScriptEditorService:UpdateSourceAsync(target, function(source)
|
|
120
205
|
local applied, result = pcall(transform, source)
|
|
121
206
|
if applied then
|
|
207
|
+
original = source
|
|
122
208
|
produced = result
|
|
123
209
|
return result
|
|
124
210
|
end
|
|
@@ -131,7 +217,7 @@ function ScriptEdit.write(target: LuaSourceContainer, transform: (string) -> str
|
|
|
131
217
|
error(pending, 0)
|
|
132
218
|
end
|
|
133
219
|
if ok then
|
|
134
|
-
confirm(target, produced)
|
|
220
|
+
confirm(target, produced, original)
|
|
135
221
|
return
|
|
136
222
|
end
|
|
137
223
|
|
|
@@ -413,7 +413,22 @@ function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
|
413
413
|
end
|
|
414
414
|
-- 0-255 is a common mistake and unambiguous to detect, so accept it.
|
|
415
415
|
local scale = if numbers[1] > 1 or numbers[2] > 1 or numbers[3] > 1 then 255 else 1
|
|
416
|
-
|
|
416
|
+
--[[
|
|
417
|
+
Snapped back to an exact 1/255 step when the text is that step
|
|
418
|
+
rounded to PRECISION places.
|
|
419
|
+
|
|
420
|
+
Part colours are stored as bytes. "0.7686" is how 196/255 prints,
|
|
421
|
+
but it is a hair under it, and the engine truncates on the way in
|
|
422
|
+
-- so a colour read and written back came out as 195, one step
|
|
423
|
+
darker each round trip. A value genuinely between steps, like 0.5,
|
|
424
|
+
is further than half a printed digit from one and is left alone.
|
|
425
|
+
]]
|
|
426
|
+
local function component(value: number): number
|
|
427
|
+
local unit = value / scale
|
|
428
|
+
local step = math.round(unit * 255) / 255
|
|
429
|
+
return if math.abs(unit - step) <= 0.5 * 10 ^ -PRECISION then step else unit
|
|
430
|
+
end
|
|
431
|
+
return true, Color3.new(component(numbers[1]), component(numbers[2]), component(numbers[3])), nil
|
|
417
432
|
end
|
|
418
433
|
if valueType == "BrickColor" then
|
|
419
434
|
-- Cast because the API definitions type this overload as a literal union
|
|
@@ -26,7 +26,9 @@ local Transport = {}
|
|
|
26
26
|
export type Status = "disconnected" | "connecting" | "connected"
|
|
27
27
|
|
|
28
28
|
type Callbacks = {
|
|
29
|
-
|
|
29
|
+
-- `deadlineMs` is when the server stops waiting for the reply, on this
|
|
30
|
+
-- machine's clock: the server sends a duration, never a time (see Command.timeoutMs).
|
|
31
|
+
onCommand: (id: string, op: string, params: { [string]: any }?, deadlineMs: number?) -> (),
|
|
30
32
|
onStatus: (status: Status, detail: string?) -> (),
|
|
31
33
|
--[[
|
|
32
34
|
Anything the bridge wants to tell this plugin that is not a command.
|
|
@@ -243,7 +245,8 @@ local function handleFrame(callbacks: Callbacks, frame: { [string]: any })
|
|
|
243
245
|
local id = frame.id
|
|
244
246
|
local op = frame.op
|
|
245
247
|
if typeof(id) == "string" and typeof(op) == "string" then
|
|
246
|
-
|
|
248
|
+
local deadline = if typeof(frame.timeoutMs) == "number" then DateTime.now().UnixTimestampMillis + frame.timeoutMs else nil
|
|
249
|
+
callbacks.onCommand(id, op, frame.params, deadline)
|
|
247
250
|
return
|
|
248
251
|
end
|
|
249
252
|
if typeof(frame.event) == "string" then
|
package/plugin/src/Undo.luau
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
20
20
|
|
|
21
|
+
local Dispatch = require(script.Parent.Dispatch)
|
|
22
|
+
|
|
21
23
|
local Undo = {}
|
|
22
24
|
|
|
23
25
|
--[[
|
|
@@ -132,13 +134,57 @@ end
|
|
|
132
134
|
is something tools tell the agent, and therefore the user, so it has to be
|
|
133
135
|
observed rather than assumed: a claim of "one Ctrl+Z" that silently degrades
|
|
134
136
|
to no undo entry at all is worse than no claim.
|
|
137
|
+
|
|
138
|
+
Refusing instead is not an option either: Studio opens no recording at all
|
|
139
|
+
inside a running playtest (measured), so a refusal would make every write
|
|
140
|
+
tool unusable against the runtime session.
|
|
141
|
+
]]
|
|
142
|
+
|
|
143
|
+
-- Opens a recording, or nil. Guarded because an engine error here must not
|
|
144
|
+
-- escape with the lock held.
|
|
145
|
+
local function begin(name: string, displayName: string): string?
|
|
146
|
+
local ok, identifier = pcall(function()
|
|
147
|
+
return ChangeHistoryService:TryBeginRecording(name, displayName)
|
|
148
|
+
end)
|
|
149
|
+
return if ok then identifier else nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
--[[
|
|
153
|
+
Runs `body` after the lock is ours, unless the request expired while it
|
|
154
|
+
waited. The server stops waiting at its timeout, so work that starts later
|
|
155
|
+
would be reported to nobody -- and an agent told "timed out" is likely to
|
|
156
|
+
retry it.
|
|
135
157
|
]]
|
|
158
|
+
local function run<T>(body: () -> T): (boolean, any)
|
|
159
|
+
return pcall(function()
|
|
160
|
+
Dispatch.checkDeadline()
|
|
161
|
+
return body()
|
|
162
|
+
end)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
-- Closes a recording. A failure here is reported, with the lock released, as
|
|
166
|
+
-- an outcome the agent must check rather than assume.
|
|
167
|
+
local function finish(identifier: string, operation: Enum.FinishRecordingOperation, acquired: boolean)
|
|
168
|
+
local ok, err = pcall(function()
|
|
169
|
+
ChangeHistoryService:FinishRecording(identifier, operation)
|
|
170
|
+
end)
|
|
171
|
+
if ok then
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
release(acquired)
|
|
175
|
+
Dispatch.fail(
|
|
176
|
+
"UNDO_FAILED",
|
|
177
|
+
string.format("Studio could not close the undo recording: %s", tostring(err)),
|
|
178
|
+
"The change may or may not have applied. Inspect the affected instances before retrying."
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
|
|
136
182
|
function Undo.record<T>(name: string, displayName: string, body: () -> T): (T, boolean)
|
|
137
183
|
local acquired = acquire()
|
|
138
184
|
|
|
139
|
-
local identifier =
|
|
185
|
+
local identifier = begin(name, displayName)
|
|
140
186
|
if not identifier then
|
|
141
|
-
local ok, result =
|
|
187
|
+
local ok, result = run(body)
|
|
142
188
|
release(acquired)
|
|
143
189
|
if not ok then
|
|
144
190
|
error(result, 0)
|
|
@@ -146,15 +192,15 @@ function Undo.record<T>(name: string, displayName: string, body: () -> T): (T, b
|
|
|
146
192
|
return result, false
|
|
147
193
|
end
|
|
148
194
|
|
|
149
|
-
local ok, result =
|
|
195
|
+
local ok, result = run(body)
|
|
150
196
|
if ok then
|
|
151
|
-
|
|
197
|
+
finish(identifier, Enum.FinishRecordingOperation.Commit, acquired)
|
|
152
198
|
release(acquired)
|
|
153
199
|
return result, true
|
|
154
200
|
end
|
|
155
201
|
|
|
156
|
-
|
|
157
|
-
dropRedo
|
|
202
|
+
finish(identifier, Enum.FinishRecordingOperation.Cancel, acquired)
|
|
203
|
+
pcall(dropRedo)
|
|
158
204
|
release(acquired)
|
|
159
205
|
error(result, 0)
|
|
160
206
|
end
|
|
@@ -167,9 +213,9 @@ end
|
|
|
167
213
|
function Undo.recordPartial<T>(name: string, displayName: string, body: () -> T): (T, boolean)
|
|
168
214
|
local acquired = acquire()
|
|
169
215
|
|
|
170
|
-
local identifier =
|
|
216
|
+
local identifier = begin(name, displayName)
|
|
171
217
|
if not identifier then
|
|
172
|
-
local ok, result =
|
|
218
|
+
local ok, result = run(body)
|
|
173
219
|
release(acquired)
|
|
174
220
|
if not ok then
|
|
175
221
|
error(result, 0)
|
|
@@ -177,8 +223,8 @@ function Undo.recordPartial<T>(name: string, displayName: string, body: () -> T)
|
|
|
177
223
|
return result, false
|
|
178
224
|
end
|
|
179
225
|
|
|
180
|
-
local ok, result =
|
|
181
|
-
|
|
226
|
+
local ok, result = run(body)
|
|
227
|
+
finish(identifier, Enum.FinishRecordingOperation.Commit, acquired)
|
|
182
228
|
release(acquired)
|
|
183
229
|
if not ok then
|
|
184
230
|
error(result, 0)
|
|
@@ -186,4 +232,22 @@ function Undo.recordPartial<T>(name: string, displayName: string, body: () -> T)
|
|
|
186
232
|
return result, true
|
|
187
233
|
end
|
|
188
234
|
|
|
235
|
+
--[[
|
|
236
|
+
Holds the mutation lock without opening a recording.
|
|
237
|
+
|
|
238
|
+
For script source, whose undo is the script editor's own, per document --
|
|
239
|
+
a recording would add an empty step, not make the edit undoable. The lock
|
|
240
|
+
is what matters: two batches writing the same scripts must not interleave
|
|
241
|
+
their writes and their rollbacks.
|
|
242
|
+
]]
|
|
243
|
+
function Undo.exclusive<T>(body: () -> T): T
|
|
244
|
+
local acquired = acquire()
|
|
245
|
+
local ok, result = run(body)
|
|
246
|
+
release(acquired)
|
|
247
|
+
if not ok then
|
|
248
|
+
error(result, 0)
|
|
249
|
+
end
|
|
250
|
+
return result
|
|
251
|
+
end
|
|
252
|
+
|
|
189
253
|
return Undo
|