@el4cteo/rbx-studio-mcp 0.3.5 → 0.3.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/dist/tools/input.js +38 -5
- package/dist/tools/input.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +44 -1
- package/plugin/src/Mirror.luau +126 -0
- package/plugin/src/ThemePicker.luau +458 -458
- package/plugin/src/Themes/Aurora.luau +161 -147
- package/plugin/src/Themes/Void.luau +266 -230
- package/plugin/src/Undo.luau +29 -0
- package/plugin/src/Visuals.luau +56 -6
- package/plugin/src/handlers/Device.luau +21 -1
- package/plugin/src/handlers/Input.luau +583 -493
- package/plugin/src/init.server.luau +552 -386
package/dist/tools/input.js
CHANGED
|
@@ -20,8 +20,19 @@ import { defineTool } from "../lib/tool.js";
|
|
|
20
20
|
* An earlier version of this file claimed the error was the ratio between the
|
|
21
21
|
* device resolution and the screenshot's, which is wrong, and wrong in a way
|
|
22
22
|
* that happens to land close to correct in the middle of the screen where it
|
|
23
|
-
* was first observed.
|
|
24
|
-
*
|
|
23
|
+
* was first observed.
|
|
24
|
+
*
|
|
25
|
+
* Applying it automatically was tried and reverted. The translation is real
|
|
26
|
+
* with nothing emulated -- (0,-58), which is exactly GuiService:GetGuiInset()
|
|
27
|
+
* -- and on an iPhone 16 in portrait, measured equal at two points 215px apart.
|
|
28
|
+
* On the SAME phone in LandscapeRight it is not a translation at all: the
|
|
29
|
+
* horizontal term inverts and scales (seen.x fitted 919 - 1.4 * sent.x), so
|
|
30
|
+
* subtracting the measured constant moved the aim from 187px out to 449px out.
|
|
31
|
+
*
|
|
32
|
+
* Nor can a better fit be sampled. Synthetic MOVES report no position --
|
|
33
|
+
* InputChanged never fires for SendMousePosition -- so the map is observable
|
|
34
|
+
* only by clicking, and probe clicks press whatever they land on. Aim once,
|
|
35
|
+
* read the delta, correct.
|
|
25
36
|
*/
|
|
26
37
|
function landingNote(landed) {
|
|
27
38
|
const dx = landed.seen.x - landed.sent.x;
|
|
@@ -31,7 +42,8 @@ function landingNote(landed) {
|
|
|
31
42
|
}
|
|
32
43
|
return (` COORDINATES ARE OFFSET: the last click was aimed at (${landed.sent.x}, ${landed.sent.y}) and the client read it at (${landed.seen.x}, ${landed.seen.y}) — ` +
|
|
33
44
|
`off by (${dx}, ${dy}). Add (${-dx}, ${-dy}) to coordinates taken from a \`screenshot\` to hit what you are aiming at. ` +
|
|
34
|
-
"
|
|
45
|
+
"That correction is a constant translation with no device emulated and in portrait, but NOT in landscape emulation, where the horizontal term inverts — " +
|
|
46
|
+
"so under an emulated device treat it as one sample: click, read this field again, and correct from the newest one rather than reusing an earlier value.");
|
|
35
47
|
}
|
|
36
48
|
export function registerInputTools(context) {
|
|
37
49
|
const { bridge } = context;
|
|
@@ -64,7 +76,15 @@ export function registerInputTools(context) {
|
|
|
64
76
|
"get an error, not a success — check where things really are with " +
|
|
65
77
|
"`character op=\"state\"`.\n\n" +
|
|
66
78
|
"Mouse coordinates are viewport pixels from the top-left, so pair this " +
|
|
67
|
-
"with `screenshot` to see what is where before clicking it."
|
|
79
|
+
"with `screenshot` to see what is where before clicking it. The client " +
|
|
80
|
+
"reads them at an offset the reply reports as `landed` — aim once, read " +
|
|
81
|
+
"where it actually landed, then correct. Under an emulated device that " +
|
|
82
|
+
"offset is large and stops being a simple translation, so re-read it " +
|
|
83
|
+
"after each click rather than reusing an earlier one.\n\n" +
|
|
84
|
+
"A `text` step types into the FOCUSED TextBox. Click the box in the same " +
|
|
85
|
+
"call, one step before the text, and the focus is taken for you; with no " +
|
|
86
|
+
"box to type into the step is reported as having done nothing rather " +
|
|
87
|
+
"than as delivered.",
|
|
68
88
|
inputSchema: {
|
|
69
89
|
steps: z
|
|
70
90
|
.array(z
|
|
@@ -91,7 +111,10 @@ export function registerInputTools(context) {
|
|
|
91
111
|
.describe("click only: which button. Defaults to left."),
|
|
92
112
|
x: z.number().optional().describe("click/move only: viewport pixels from the left."),
|
|
93
113
|
y: z.number().optional().describe("click/move only: viewport pixels from the top."),
|
|
94
|
-
text: z
|
|
114
|
+
text: z
|
|
115
|
+
.string()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("text only: the string to type. Goes to the focused TextBox — click it first, in the same call."),
|
|
95
118
|
hold: z
|
|
96
119
|
.number()
|
|
97
120
|
.min(0)
|
|
@@ -163,6 +186,16 @@ export function registerInputTools(context) {
|
|
|
163
186
|
else {
|
|
164
187
|
parts.push('Read the result with `character op="state"`, `console`, or `screenshot`.');
|
|
165
188
|
}
|
|
189
|
+
/*
|
|
190
|
+
* A step that ran and changed nothing is the failure this whole file
|
|
191
|
+
* exists to avoid reporting as success, so the client's own words about
|
|
192
|
+
* it go at the front where they cannot be skimmed past.
|
|
193
|
+
*/
|
|
194
|
+
if (Array.isArray(response.notes)) {
|
|
195
|
+
for (const line of response.notes) {
|
|
196
|
+
parts.unshift(`WARNING: ${line}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
166
199
|
const note = parts.join(" ");
|
|
167
200
|
return json(response, note);
|
|
168
201
|
});
|
package/dist/tools/input.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAmB,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAoB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/plugin/src/Config.luau
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
local Config = {}
|
|
11
11
|
|
|
12
|
-
Config.PLUGIN_VERSION = "0.3.
|
|
12
|
+
Config.PLUGIN_VERSION = "0.3.6"
|
|
13
13
|
|
|
14
14
|
-- Fingerprint of plugin/src, stamped in by scripts/build-plugin.mjs. The server
|
|
15
15
|
-- computes the same hash from its own copy of the sources and compares, so a
|
package/plugin/src/Console.luau
CHANGED
|
@@ -49,6 +49,43 @@ local MAX_RECORDS = 300
|
|
|
49
49
|
]]
|
|
50
50
|
local PALETTE: Themes.Palette = Themes.palette()
|
|
51
51
|
|
|
52
|
+
--[[
|
|
53
|
+
An optional listener on everything this console is told.
|
|
54
|
+
|
|
55
|
+
Exists for `Mirror`, which relays a playtest server's activity to the client
|
|
56
|
+
view that cannot reach the bridge itself. Kept as one hook rather than as
|
|
57
|
+
calls sprinkled through the file so there is a single place where "what the
|
|
58
|
+
console was told" is defined, and so the client half -- which sets no
|
|
59
|
+
observer -- cannot echo what it replays back into the channel.
|
|
60
|
+
]]
|
|
61
|
+
local observer: ((string, { any }) -> ())? = nil
|
|
62
|
+
|
|
63
|
+
function Console.setObserver(fn: ((string, { any }) -> ())?)
|
|
64
|
+
observer = fn
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
local function notify(kind: string, arguments: { any })
|
|
68
|
+
local listener = observer
|
|
69
|
+
if listener ~= nil then
|
|
70
|
+
task.spawn(listener, kind, arguments)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
--[[
|
|
75
|
+
Set on the console that is REPLAYING someone else's events.
|
|
76
|
+
|
|
77
|
+
A replayed call must not grow its own consequences. `recordCall` schedules
|
|
78
|
+
the "agent idle" summary, so a mirrored session scheduled one of its own and
|
|
79
|
+
then received the original over the channel too -- the same line twice, a
|
|
80
|
+
few pixels apart, which is exactly the sort of thing that makes a panel look
|
|
81
|
+
broken. The origin is the only session entitled to decide the burst ended.
|
|
82
|
+
]]
|
|
83
|
+
local mirroring = false
|
|
84
|
+
|
|
85
|
+
function Console.setMirroring(value: boolean)
|
|
86
|
+
mirroring = value
|
|
87
|
+
end
|
|
88
|
+
|
|
52
89
|
-- Sigil plus colour per level. The sigil is what makes the log scannable at a
|
|
53
90
|
-- glance; colour alone fails for anyone who cannot separate red from green.
|
|
54
91
|
-- Named by palette key rather than by colour, because this table is built once
|
|
@@ -385,6 +422,7 @@ function Console.log(level: Level, message: string, detail: string?)
|
|
|
385
422
|
table.remove(state.records, 1)
|
|
386
423
|
end
|
|
387
424
|
redraw()
|
|
425
|
+
notify("log", { level, message, detail })
|
|
388
426
|
end
|
|
389
427
|
|
|
390
428
|
--[[
|
|
@@ -422,6 +460,7 @@ function Console.beginCall(title: string, kind: string)
|
|
|
422
460
|
Visuals.setCaption(title)
|
|
423
461
|
Visuals.setKind(PALETTE[look.key], look.urgency)
|
|
424
462
|
refreshCounters()
|
|
463
|
+
notify("beginCall", { title, kind })
|
|
425
464
|
end
|
|
426
465
|
|
|
427
466
|
--[[
|
|
@@ -446,6 +485,7 @@ function Console.recordCall(ok: boolean, milliseconds: number)
|
|
|
446
485
|
-- The band plots it: bar height is how long it took, colour is whether it
|
|
447
486
|
-- worked, and a failure knocks the solid off its axis as well.
|
|
448
487
|
Visuals.recordCall(milliseconds, ok, title)
|
|
488
|
+
notify("recordCall", { ok, milliseconds, title })
|
|
449
489
|
|
|
450
490
|
--[[
|
|
451
491
|
Says so when the session goes quiet, once per burst.
|
|
@@ -459,6 +499,9 @@ function Console.recordCall(ok: boolean, milliseconds: number)
|
|
|
459
499
|
]]
|
|
460
500
|
state.generation += 1
|
|
461
501
|
local mine = state.generation
|
|
502
|
+
if mirroring then
|
|
503
|
+
return
|
|
504
|
+
end
|
|
462
505
|
task.delay(QUIET_SECONDS, function()
|
|
463
506
|
if state.generation ~= mine or state.calls == 0 then
|
|
464
507
|
return
|
|
@@ -604,7 +647,7 @@ function Console.setStatus(status: string, meta: string)
|
|
|
604
647
|
local color = if status == "connected"
|
|
605
648
|
then PALETTE.green
|
|
606
649
|
elseif status == "connecting" then PALETTE.amber
|
|
607
|
-
elseif status == "standby" then PALETTE.dim
|
|
650
|
+
elseif status == "standby" or status == "mirroring" then PALETTE.dim
|
|
608
651
|
else PALETTE.red
|
|
609
652
|
|
|
610
653
|
dot.TextColor3 = color
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Carries console activity from a playtest's SERVER view to its CLIENT view.
|
|
4
|
+
|
|
5
|
+
Pressing Play gives this plugin three lives: the editor's, the playtest
|
|
6
|
+
server's, and the playtest client's. Studio shows whichever belongs to the
|
|
7
|
+
view you are looking at, and it defaults to the client -- which is the one
|
|
8
|
+
life that can do nothing. Roblox forbids HTTP from a client session, so that
|
|
9
|
+
panel has no way to see the bridge, and it sat on a standby notice while the
|
|
10
|
+
agent worked a few feet away in a view the user was not looking at.
|
|
11
|
+
|
|
12
|
+
The two DataModels share no memory, no globals and no services. The only
|
|
13
|
+
channel between them is the game's own replication, so that is what this
|
|
14
|
+
uses: the server plugin puts a RemoteEvent in ReplicatedStorage and fires
|
|
15
|
+
each console event across it; the client plugin listens and replays them
|
|
16
|
+
into its own console. The prism reacts, the log fills, and the view the user
|
|
17
|
+
actually has open stops lying about what is happening.
|
|
18
|
+
|
|
19
|
+
The cost is honest and worth stating: this plugin writes one instance into
|
|
20
|
+
the running game. It is `Archivable = false` so it can never be saved with
|
|
21
|
+
the place, it exists only while a playtest is running, and it goes when the
|
|
22
|
+
playtest's DataModel does. Nothing reads it but the client half of this
|
|
23
|
+
plugin, and nothing is sent up from the client -- the traffic is one-way, so
|
|
24
|
+
a game's own scripts cannot use it to reach the editor.
|
|
25
|
+
]]
|
|
26
|
+
|
|
27
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
28
|
+
local RunService = game:GetService("RunService")
|
|
29
|
+
|
|
30
|
+
--[[
|
|
31
|
+
Named to be recognised in the Explorer rather than to be hidden.
|
|
32
|
+
|
|
33
|
+
Someone will see this appear in ReplicatedStorage mid-playtest and want to
|
|
34
|
+
know what put it there; a cryptic name turns that into a hunt. The prefix is
|
|
35
|
+
unlikely enough to collide with a real game's instances.
|
|
36
|
+
]]
|
|
37
|
+
local CHANNEL = "__rbxStudioMcpMirror"
|
|
38
|
+
|
|
39
|
+
local Mirror = {}
|
|
40
|
+
|
|
41
|
+
local remote: RemoteEvent? = nil
|
|
42
|
+
|
|
43
|
+
--[[
|
|
44
|
+
True only in the half of a playtest that can reach the bridge.
|
|
45
|
+
|
|
46
|
+
`IsRunning` separates a playtest from the editor, and `IsServer` separates
|
|
47
|
+
the two halves of it. The editor session must not do any of this: it has no
|
|
48
|
+
playtest to mirror and writing to its ReplicatedStorage would put the
|
|
49
|
+
instance in the user's actual place.
|
|
50
|
+
]]
|
|
51
|
+
function Mirror.isPlaytestServer(): boolean
|
|
52
|
+
return RunService:IsRunning() and RunService:IsServer()
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
function Mirror.isPlaytestClient(): boolean
|
|
56
|
+
return RunService:IsRunning() and RunService:IsClient()
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
--[[
|
|
60
|
+
Opens the channel from the playtest server.
|
|
61
|
+
|
|
62
|
+
A leftover from an earlier test in the same DataModel is replaced rather
|
|
63
|
+
than reused: reusing one whose connections are gone looks like a working
|
|
64
|
+
channel that silently delivers nothing.
|
|
65
|
+
]]
|
|
66
|
+
function Mirror.startServer()
|
|
67
|
+
if not Mirror.isPlaytestServer() then
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
local existing = ReplicatedStorage:FindFirstChild(CHANNEL)
|
|
71
|
+
if existing ~= nil then
|
|
72
|
+
existing:Destroy()
|
|
73
|
+
end
|
|
74
|
+
local event = Instance.new("RemoteEvent")
|
|
75
|
+
event.Name = CHANNEL
|
|
76
|
+
-- Never saved with the place, whatever the user does while the test runs.
|
|
77
|
+
event.Archivable = false
|
|
78
|
+
event.Parent = ReplicatedStorage
|
|
79
|
+
remote = event
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
--[[
|
|
83
|
+
Sends one console event, and never fails loudly.
|
|
84
|
+
|
|
85
|
+
This is decoration on top of the console. A replication error here must not
|
|
86
|
+
take down the call it was reporting on, so every send is guarded and a dead
|
|
87
|
+
channel simply stops mirroring.
|
|
88
|
+
]]
|
|
89
|
+
function Mirror.send(kind: string, arguments: { any })
|
|
90
|
+
local event = remote
|
|
91
|
+
if event == nil then
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
pcall(function()
|
|
95
|
+
event:FireAllClients(kind, arguments)
|
|
96
|
+
end)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
--[[
|
|
100
|
+
Listens on the playtest client.
|
|
101
|
+
|
|
102
|
+
`WaitForChild` with a timeout rather than an indefinite wait: if the server
|
|
103
|
+
half never opens the channel the client should go quiet, not hang a thread
|
|
104
|
+
for the length of the playtest.
|
|
105
|
+
]]
|
|
106
|
+
function Mirror.startClient(onMessage: (string, { any }) -> ())
|
|
107
|
+
if not Mirror.isPlaytestClient() then
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
task.spawn(function()
|
|
111
|
+
local event = ReplicatedStorage:WaitForChild(CHANNEL, 30)
|
|
112
|
+
if event == nil or not event:IsA("RemoteEvent") then
|
|
113
|
+
return
|
|
114
|
+
end
|
|
115
|
+
event.OnClientEvent:Connect(function(kind, arguments)
|
|
116
|
+
-- Shape-checked because anything on the wire can be forged by a
|
|
117
|
+
-- script in the place being tested. Nothing here is trusted enough
|
|
118
|
+
-- to index without looking.
|
|
119
|
+
if typeof(kind) == "string" and typeof(arguments) == "table" then
|
|
120
|
+
onMessage(kind, arguments :: { any })
|
|
121
|
+
end
|
|
122
|
+
end)
|
|
123
|
+
end)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
return Mirror
|