@el4cteo/rbx-studio-mcp 0.3.1 → 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.
Files changed (43) hide show
  1. package/README.md +20 -26
  2. package/dist/index.js +1 -1
  3. package/dist/lib/format.js +18 -2
  4. package/dist/lib/format.js.map +1 -1
  5. package/dist/tools/api.js +20 -1
  6. package/dist/tools/api.js.map +1 -1
  7. package/dist/tools/debug.js +18 -8
  8. package/dist/tools/debug.js.map +1 -1
  9. package/dist/tools/discover.js +5 -0
  10. package/dist/tools/discover.js.map +1 -1
  11. package/dist/tools/input.js +38 -5
  12. package/dist/tools/input.js.map +1 -1
  13. package/dist/tools/instances.js +49 -2
  14. package/dist/tools/instances.js.map +1 -1
  15. package/dist/tools/perf.js +9 -8
  16. package/dist/tools/perf.js.map +1 -1
  17. package/dist/tools/scripts.js +10 -1
  18. package/dist/tools/scripts.js.map +1 -1
  19. package/package.json +1 -1
  20. package/plugin/src/Config.luau +1 -1
  21. package/plugin/src/Console.luau +301 -117
  22. package/plugin/src/Mirror.luau +126 -0
  23. package/plugin/src/Serialize.luau +43 -3
  24. package/plugin/src/ThemePicker.luau +458 -0
  25. package/plugin/src/Themes/Aurora.luau +161 -0
  26. package/plugin/src/Themes/Blueprint.luau +213 -0
  27. package/plugin/src/Themes/Draw.luau +177 -0
  28. package/plugin/src/Themes/Lattice.luau +304 -0
  29. package/plugin/src/Themes/Nebula.luau +182 -0
  30. package/plugin/src/Themes/Observatory.luau +200 -0
  31. package/plugin/src/Themes/Orbit.luau +268 -0
  32. package/plugin/src/Themes/Phosphor.luau +200 -0
  33. package/plugin/src/Themes/Theme.luau +121 -0
  34. package/plugin/src/Themes/Void.luau +266 -0
  35. package/plugin/src/Themes/init.luau +116 -0
  36. package/plugin/src/Undo.luau +29 -0
  37. package/plugin/src/Visuals.luau +838 -907
  38. package/plugin/src/handlers/Device.luau +21 -1
  39. package/plugin/src/handlers/Discover.luau +75 -1
  40. package/plugin/src/handlers/Exec.luau +17 -5
  41. package/plugin/src/handlers/Input.luau +583 -493
  42. package/plugin/src/handlers/Instances.luau +35 -3
  43. package/plugin/src/init.server.luau +209 -11
@@ -0,0 +1,266 @@
1
+ --!strict
2
+ --[[
3
+ Void -- an accretion disc, and the most literal of the eight.
4
+
5
+ A dark core with matter spiralling into it. Orbital speed rises as radius
6
+ falls, the way it actually does, so the inner ring visibly outruns the outer
7
+ one and the disc shears instead of turning as a plate. That single detail is
8
+ what separates it from a spinning ring of dots.
9
+
10
+ The trace is where the metaphor pays for itself. A slow call is drawn
11
+ stretched and shifted red, so latency is not a height you have to compare
12
+ against its neighbours -- it is a thing visibly being pulled apart. A
13
+ forty-bar chart makes you measure; this makes you look.
14
+
15
+ It is also the least legible preset in the set, and that is a real trade
16
+ rather than an oversight: the disc is dark, the motion is continuous, and
17
+ there is no baseline to read heights against. It is here because someone
18
+ will want it, not because it is the best instrument.
19
+ ]]
20
+
21
+ local Draw = require(script.Parent.Draw)
22
+ local Theme = require(script.Parent.Theme)
23
+
24
+ local MOTES = 34
25
+ local RINGS = 4
26
+
27
+ --[[
28
+ How far the disc reaches, and how flat it is seen.
29
+
30
+ The reach was already near the cell's limit; the disc read as small because
31
+ it was a HAIRLINE -- flattened to 0.3, four rings of it occupied about 10px
32
+ of vertical space in a 40px cell, so a wide thin smear was doing the work of
33
+ a picture. Opening the viewing angle to 0.48 nearly doubles the height for
34
+ no extra width, which is the axis that actually had room spare.
35
+ ]]
36
+ local DISC_SPREAD = 0.54
37
+ local FLATTEN = 0.48
38
+
39
+ -- The disc barely breathes: same reasoning as Orbit. A ring sized to fill the
40
+ -- cell at rest would be cut off by the clip at every overshoot, so the CORE
41
+ -- carries the spring and the matter around it holds its radius.
42
+ local DISC_BREATH = 0.1
43
+
44
+ local Void = {}
45
+
46
+ Void.id = "void"
47
+ Void.label = "Void"
48
+ Void.blurb = "Accretion disc"
49
+
50
+ Void.palette = {
51
+ background = Color3.fromRGB(6, 5, 10),
52
+ surface = Color3.fromRGB(13, 11, 20),
53
+ rule = Color3.fromRGB(168, 108, 236),
54
+ text = Color3.fromRGB(206, 200, 222),
55
+ dim = Color3.fromRGB(84, 78, 102),
56
+ green = Color3.fromRGB(120, 220, 176),
57
+ red = Color3.fromRGB(252, 96, 118),
58
+ amber = Color3.fromRGB(250, 174, 108),
59
+ cyan = Color3.fromRGB(126, 202, 244),
60
+ violet = Color3.fromRGB(178, 122, 246),
61
+ }
62
+
63
+ local motes: { Frame } = {}
64
+ local core: Frame? = nil
65
+ local shadow: Frame? = nil
66
+
67
+ --[[
68
+ Accumulated orbital phase, in the disc's own time.
69
+
70
+ This is the fix for the one thing that made this preset unwatchable, and it
71
+ is worth stating plainly because the broken version looked reasonable.
72
+
73
+ The angle used to be `ctx.clock * speed * (1 + ctx.energy)` -- absolute wall
74
+ time multiplied by a rate that CHANGES. Multiplying a large clock by a moving
75
+ rate does not speed the disc up; it teleports every mote. `os.clock()` is
76
+ seconds since Studio started, so after an hour it is about 3600, and energy
77
+ falls by `delta * 0.9` every frame after a command: at 60fps that is 0.015 of
78
+ rate change per frame, which at ring one moved the phase by roughly 29
79
+ radians -- almost five whole revolutions -- BETWEEN CONSECUTIVE FRAMES. For
80
+ the second or two after every call the disc was not orbiting at all, it was
81
+ strobing, and it got worse the longer Studio had been open, which is why it
82
+ looked fine when it was written.
83
+
84
+ Integrating the rate instead makes speed a speed. A change in energy now
85
+ changes how fast the phase grows, and the phase itself stays continuous, so
86
+ the disc accelerates rather than jumping.
87
+ ]]
88
+ local phase = 0
89
+
90
+ --[[
91
+ Which ring a mote belongs to, and where it sits on that ring.
92
+
93
+ Distributed by index rather than randomly so every ring gets a similar share
94
+ -- a random assignment leaves visible gaps at forty motes, and a gap in a
95
+ disc reads as a rendering fault rather than as structure.
96
+ ]]
97
+ local function ringOf(index: number): (number, number)
98
+ local ring = 1 + (index - 1) % RINGS
99
+ local along = Draw.hash(index, 41)
100
+ return ring, along
101
+ end
102
+
103
+ function Void.mount(cell: Frame)
104
+ local dark = Instance.new("Frame")
105
+ dark.Name = "Shadow"
106
+ dark.AnchorPoint = Vector2.new(0.5, 0.5)
107
+ dark.BorderSizePixel = 0
108
+ dark.Parent = cell
109
+ local darkCorner = Instance.new("UICorner")
110
+ darkCorner.CornerRadius = UDim.new(1, 0)
111
+ darkCorner.Parent = dark
112
+ shadow = dark
113
+
114
+ motes = Draw.pool(cell, MOTES, "Mote", true)
115
+
116
+ --[[
117
+ The core is built LAST so it draws over the motes.
118
+
119
+ That is the entire illusion: matter passing behind the core has to
120
+ disappear behind something, and with the core underneath, motes crossed
121
+ it and the disc read as a flat ring with a dot in the middle.
122
+ ]]
123
+ local sphere = Instance.new("Frame")
124
+ sphere.Name = "Core"
125
+ sphere.AnchorPoint = Vector2.new(0.5, 0.5)
126
+ sphere.BorderSizePixel = 0
127
+ sphere.Parent = cell
128
+ local corner = Instance.new("UICorner")
129
+ corner.CornerRadius = UDim.new(1, 0)
130
+ corner.Parent = sphere
131
+ core = sphere
132
+ phase = 0
133
+ end
134
+
135
+ function Void.unmount()
136
+ motes = {}
137
+ core = nil
138
+ shadow = nil
139
+ phase = 0
140
+ end
141
+
142
+ function Void.paint(ctx: Theme.Context, delta: number)
143
+ --[[
144
+ Advance the disc by this frame's worth of turning.
145
+
146
+ `pace` matches the rest of the set: past the quiet threshold the session
147
+ has stopped, and a disc still spiralling at working speed claims an
148
+ activity that is not happening. Applied to the RATE, so crossing the
149
+ threshold slows the disc instead of jumping it.
150
+ ]]
151
+ local pace = if ctx.quiet > 20 then 0.25 else 1
152
+ phase += delta * (1 + ctx.energy) * pace
153
+
154
+ local breath = (1 - DISC_BREATH) + ctx.scale * DISC_BREATH
155
+ local scale = ctx.size * DISC_SPREAD * breath
156
+ -- Lensing: a failure warps the disc rather than shaking the panel, which on
157
+ -- a strip this small would read as a glitch.
158
+ local warp = 1 + ctx.shake * 0.28
159
+
160
+ for index = 1, MOTES do
161
+ local frame = motes[index]
162
+ if frame == nil then
163
+ continue
164
+ end
165
+ local ring, along = ringOf(index)
166
+ local radius = (0.3 + ring * 0.14) * scale * warp
167
+
168
+ --[[
169
+ Keplerian, near enough: inner orbits are faster. Anything uniform
170
+ turns the disc as a rigid plate, and the shear is what says the
171
+ thing at the centre is pulling rather than spinning.
172
+ ]]
173
+ local speed = 2.4 / (0.3 + ring * 0.14)
174
+ local turn = along * math.pi * 2 + phase * speed * 0.1
175
+
176
+ -- Flattened on Y: a disc seen nearly edge-on, not a set of circles.
177
+ local x = math.cos(turn) * radius
178
+ local y = math.sin(turn) * radius * FLATTEN
179
+ -- Depth from the same angle, so a mote on the far side dims.
180
+ local near = (math.sin(turn) + 1) / 2
181
+
182
+ local heat = 1 - ring / RINGS
183
+ Draw.dot(
184
+ frame,
185
+ ctx.centre + Vector2.new(x, y),
186
+ math.max(2, math.floor(1.5 + heat * 2.5 + ctx.flare * 1.5)),
187
+ ctx.palette.violet
188
+ :Lerp(ctx.palette.amber, heat)
189
+ :Lerp(Color3.new(1, 1, 1), ctx.flare * 0.4),
190
+ math.clamp(0.82 - near * 0.45 - heat * 0.2 - ctx.flare * 0.25, 0, 1)
191
+ )
192
+ end
193
+
194
+ local halo = shadow
195
+ if halo ~= nil then
196
+ Draw.dot(
197
+ halo,
198
+ ctx.centre,
199
+ math.max(12, math.floor(16 * ctx.scale + ctx.energy * 5)),
200
+ ctx.palette.violet,
201
+ 0.88
202
+ )
203
+ end
204
+
205
+ local sphere = core
206
+ if sphere ~= nil then
207
+ -- The core does not brighten on a reply. Everything around it does, and
208
+ -- the hole staying black through a flare is what sells it as a hole.
209
+ Draw.dot(
210
+ sphere,
211
+ ctx.centre,
212
+ math.max(6, math.floor(9 * ctx.scale)),
213
+ ctx.palette.background,
214
+ 0
215
+ )
216
+ end
217
+ end
218
+
219
+ --[[
220
+ History falling inward.
221
+
222
+ A slow call is drawn taller AND redder AND further from the baseline, so
223
+ three channels carry the same fact. That is redundant on purpose: the disc
224
+ behind it is busy, and one channel would be lost in it.
225
+ ]]
226
+ function Void.paintSlot(slot: Theme.Slot, ctx: Theme.Context)
227
+ local frame = slot.frame
228
+ local recency = 1 - slot.age
229
+ local height = math.max(2, math.floor(2 + slot.weight * 18))
230
+
231
+ frame.AnchorPoint = Vector2.new(0.5, 1)
232
+ frame.Size = UDim2.fromOffset(2, height)
233
+ -- Lifted off the baseline in proportion to how slow it was: the bar has
234
+ -- started to fall in.
235
+ frame.Position = UDim2.new(
236
+ frame.Position.X.Scale,
237
+ frame.Position.X.Offset,
238
+ 1,
239
+ -math.floor(slot.weight * 4)
240
+ )
241
+ frame.BackgroundColor3 = if slot.ok
242
+ then ctx.palette.amber:Lerp(ctx.palette.red, slot.weight)
243
+ else ctx.palette.red
244
+ frame.BackgroundTransparency = math.clamp(0.7 - recency * 0.6, 0, 1)
245
+ frame.Rotation = 0
246
+ end
247
+
248
+ function Void.swatch(frame: Frame, palette: Theme.Palette)
249
+ local centre = Vector2.new(20, 12)
250
+ local pool = Draw.pool(frame, 26, "Mote", true)
251
+ for index = 1, 24 do
252
+ local ring = 1 + (index - 1) % 3
253
+ local turn = Draw.hash(index, 43) * math.pi * 2
254
+ local radius = 5 + ring * 3.4
255
+ Draw.dot(
256
+ pool[index],
257
+ centre + Vector2.new(math.cos(turn) * radius, math.sin(turn) * radius * FLATTEN),
258
+ 1 + math.floor((3 - ring) * 0.7),
259
+ palette.violet:Lerp(palette.amber, 1 - ring / 3),
260
+ 0.45
261
+ )
262
+ end
263
+ Draw.dot(pool[25], centre, 9, palette.background, 0)
264
+ end
265
+
266
+ return Void
@@ -0,0 +1,116 @@
1
+ --!strict
2
+ --[[
3
+ The preset registry: which themes exist, which is active, and who to tell
4
+ when that changes.
5
+
6
+ Deliberately dumb. It holds no drawing code and no Instances -- it maps ids
7
+ to modules, remembers one id, and fires listeners. Everything visual lives
8
+ in the preset modules and everything stateful lives in Visuals, so this file
9
+ is the one place you can look to answer "what themes are there" without
10
+ reading eight renderers.
11
+
12
+ Persistence is not done here either. `plugin` is a script-level global that
13
+ ModuleScripts do not get, so init.server.luau reads the stored id at startup
14
+ and writes it back from `onChanged`. That keeps the only Studio-specific
15
+ dependency in the one file that already has it.
16
+ ]]
17
+
18
+ local Theme = require(script.Theme)
19
+
20
+ local Themes = {}
21
+
22
+ export type Theme = Theme.Theme
23
+ export type Palette = Theme.Palette
24
+ export type Context = Theme.Context
25
+ export type Slot = Theme.Slot
26
+
27
+ --[[
28
+ Presentation order in the picker, which is not alphabetical.
29
+
30
+ Lattice first because it is the default and the one people return to. Then
31
+ the two dark presets closest to it, then the colourful ones, then the two
32
+ that abandon the aesthetic entirely. A user scanning the drawer should be
33
+ able to stop as soon as the list stops looking like what they came for.
34
+ ]]
35
+ local ORDER: { Theme.Theme } = {
36
+ require(script.Lattice),
37
+ require(script.Observatory),
38
+ require(script.Orbit),
39
+ require(script.Void),
40
+ require(script.Nebula),
41
+ require(script.Aurora),
42
+ require(script.Phosphor),
43
+ require(script.Blueprint),
44
+ }
45
+
46
+ local byId: { [string]: Theme.Theme } = {}
47
+ for _, theme in ORDER do
48
+ byId[theme.id] = theme
49
+ end
50
+
51
+ local DEFAULT = "lattice"
52
+ local activeId = DEFAULT
53
+ local listeners: { (Theme.Theme) -> () } = {}
54
+
55
+ function Themes.list(): { Theme.Theme }
56
+ return ORDER
57
+ end
58
+
59
+ --[[
60
+ Looks up a preset, falling back to the default rather than erroring.
61
+
62
+ The id can come from a saved setting written by an older or newer build, so
63
+ an unknown one is expected rather than exceptional -- and a console that
64
+ refuses to start because a theme was renamed would be a bad trade for a
65
+ cosmetic feature.
66
+ ]]
67
+ function Themes.get(id: string?): Theme.Theme
68
+ if id ~= nil then
69
+ local found = byId[id]
70
+ if found ~= nil then
71
+ return found
72
+ end
73
+ end
74
+ return byId[DEFAULT]
75
+ end
76
+
77
+ function Themes.active(): Theme.Theme
78
+ return Themes.get(activeId)
79
+ end
80
+
81
+ function Themes.palette(): Theme.Palette
82
+ return Themes.active().palette
83
+ end
84
+
85
+ function Themes.activeId(): string
86
+ return activeId
87
+ end
88
+
89
+ --[[
90
+ Switches presets and tells everyone who cares.
91
+
92
+ Returns whether anything changed, so a caller re-selecting the current theme
93
+ does not pay for a full remount and a repaint of the log.
94
+ ]]
95
+ function Themes.use(id: string): boolean
96
+ local theme = Themes.get(id)
97
+ if theme.id == activeId then
98
+ return false
99
+ end
100
+ activeId = theme.id
101
+ for _, listener in listeners do
102
+ listener(theme)
103
+ end
104
+ return true
105
+ end
106
+
107
+ --[[
108
+ Registers a listener. There is no unsubscribe: every listener here lives as
109
+ long as the plugin does, and an unsubscribe nobody calls is a function that
110
+ rots rather than one that helps.
111
+ ]]
112
+ function Themes.onChanged(listener: (Theme.Theme) -> ())
113
+ table.insert(listeners, listener)
114
+ end
115
+
116
+ return Themes
@@ -20,6 +20,34 @@ local ChangeHistoryService = game:GetService("ChangeHistoryService")
20
20
 
21
21
  local Undo = {}
22
22
 
23
+ --[[
24
+ Drops the redo entry a cancelled recording leaves behind.
25
+
26
+ `Cancel` reverts the values but Studio still files the recording on the REDO
27
+ stack, so a batch this server reported as "nothing was changed, the whole
28
+ batch was rolled back" was one Ctrl+Y away from coming back -- as exactly the
29
+ half-applied state the cancel existed to prevent. Measured: a `modify` that
30
+ set Material on two parts and then hit a bad Size left `GetCanRedo` true, and
31
+ a single redo re-applied both Materials.
32
+
33
+ There is no API to pop one redo entry, but an ordinary undo stack discards
34
+ the redo branch as soon as a new action is committed -- and an EMPTY
35
+ recording counts as that action while adding no undo step of its own
36
+ (measured: after the empty commit, one undo still reached the change made
37
+ before the cancelled batch). So the cheapest correct fix is to commit
38
+ nothing.
39
+ ]]
40
+ local function dropRedo()
41
+ if not ChangeHistoryService:GetCanRedo() then
42
+ return
43
+ end
44
+ local identifier = ChangeHistoryService:TryBeginRecording("MCPFlushRedo", "MCP")
45
+ if identifier == nil then
46
+ return
47
+ end
48
+ ChangeHistoryService:FinishRecording(identifier, Enum.FinishRecordingOperation.Commit)
49
+ end
50
+
23
51
  --[[
24
52
  Runs `body` inside a named recording. Returns its result, and whether a
25
53
  recording was actually opened.
@@ -47,6 +75,7 @@ function Undo.record<T>(name: string, displayName: string, body: () -> T): (T, b
47
75
  end
48
76
 
49
77
  ChangeHistoryService:FinishRecording(identifier, Enum.FinishRecordingOperation.Cancel)
78
+ dropRedo()
50
79
  error(result, 0)
51
80
  end
52
81