@el4cteo/rbx-studio-mcp 0.1.6 → 0.2.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 +67 -5
- package/dist/bridge/api.js +29 -0
- package/dist/bridge/api.js.map +1 -1
- package/dist/bridge/remote.js +35 -0
- package/dist/bridge/remote.js.map +1 -1
- package/dist/bridge/rpc.js +81 -2
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/bridge/server.js +60 -3
- package/dist/bridge/server.js.map +1 -1
- package/dist/index.js +86 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/protocol.js +23 -0
- package/dist/lib/protocol.js.map +1 -1
- package/dist/tools/discover.js +7 -1
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/input.js +63 -1
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/instances.js +9 -1
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +73 -16
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/playtest.js +8 -1
- package/dist/tools/playtest.js.map +1 -1
- package/dist/tools/session.js +32 -6
- package/dist/tools/session.js.map +1 -1
- package/dist/tools/world.js +9 -3
- package/dist/tools/world.js.map +1 -1
- package/package.json +2 -2
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +204 -4
- package/plugin/src/Net.luau +23 -1
- package/plugin/src/Phrase.luau +158 -5
- package/plugin/src/Transport.luau +84 -19
- package/plugin/src/Visuals.luau +217 -20
- package/plugin/src/handlers/Debug.luau +63 -6
- package/plugin/src/handlers/Geometry.luau +20 -1
- package/plugin/src/handlers/Input.luau +41 -1
- package/plugin/src/handlers/Perf.luau +662 -645
- package/plugin/src/handlers/World.luau +19 -0
- package/plugin/src/init.server.luau +48 -11
- package/scripts/check-plugin.mjs +29 -1
- package/scripts/test-bridge.mjs +104 -0
- package/scripts/test-transport.mjs +58 -0
package/plugin/src/Visuals.luau
CHANGED
|
@@ -123,10 +123,36 @@ local INNER_SCALE = 0.42
|
|
|
123
123
|
-- The solid was originally as tall as the band and read as looming: it crowded
|
|
124
124
|
-- the caption and made a 46px strip feel like a panel. It is an indicator, not
|
|
125
125
|
-- the subject, so it sits small with air around it.
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
--
|
|
127
|
+
-- Both grew when the solid learned to overshoot. `cell` clips its descendants,
|
|
128
|
+
-- so a shape that swells to 1.4x has to have somewhere to swell into, and the
|
|
129
|
+
-- extra band height goes to the trace, whose bars are what people try to point
|
|
130
|
+
-- at.
|
|
131
|
+
Visuals.BAND_HEIGHT = 52
|
|
132
|
+
local CELL = 40
|
|
128
133
|
local GUTTER = 16
|
|
129
134
|
|
|
135
|
+
--[[
|
|
136
|
+
The solid's resting radius as a fraction of the cell.
|
|
137
|
+
|
|
138
|
+
Tuned against the new cell so resting size is unchanged: 40 x 0.185 is 7.4px
|
|
139
|
+
where 34 x 0.2 was 6.8px. At full overshoot the perspective divide pushes a
|
|
140
|
+
near vertex to roughly 15px, which still clears the cell's 20px half-width.
|
|
141
|
+
]]
|
|
142
|
+
local BASE_RADIUS = 0.185
|
|
143
|
+
|
|
144
|
+
--[[
|
|
145
|
+
Spring constants for the scale. Stiff enough that a call reads as a snap
|
|
146
|
+
rather than a swell, damped just under critical so a reply overshoots once
|
|
147
|
+
and settles instead of ringing.
|
|
148
|
+
]]
|
|
149
|
+
local SPRING = 90
|
|
150
|
+
local DAMPING = 14
|
|
151
|
+
|
|
152
|
+
-- How long the session must go without a command before the solid winds down.
|
|
153
|
+
-- Matches the console's idle notice, so the picture and the log agree.
|
|
154
|
+
local QUIET_AFTER = 20
|
|
155
|
+
|
|
130
156
|
-- How many calls the trace remembers. Forty is about a screen's width of bars
|
|
131
157
|
-- at two pixels each, and about as far back as anyone reads a trend.
|
|
132
158
|
local TRACE = 40
|
|
@@ -141,6 +167,10 @@ local SLOW_MS = 400
|
|
|
141
167
|
-- rather than falling into a dead gap.
|
|
142
168
|
local BAR_STRIDE = 5
|
|
143
169
|
|
|
170
|
+
-- Height of the trace, and so of every bar's hit column. The taller band spends
|
|
171
|
+
-- its extra pixels here: a 19px column was a small thing to aim a pointer at.
|
|
172
|
+
local TRACE_HEIGHT = 26
|
|
173
|
+
|
|
144
174
|
type Bar = {
|
|
145
175
|
frame: Frame,
|
|
146
176
|
--[[
|
|
@@ -153,6 +183,15 @@ type Bar = {
|
|
|
153
183
|
hit: TextButton,
|
|
154
184
|
milliseconds: number,
|
|
155
185
|
ok: boolean,
|
|
186
|
+
--[[
|
|
187
|
+
What ran, in the same words the log uses.
|
|
188
|
+
|
|
189
|
+
The trace plotted durations with nothing to attach them to, so a tall bar
|
|
190
|
+
raised the question it could not answer: forty numbers and no way to tell
|
|
191
|
+
which of them was the script edit. Carried per bar rather than looked up,
|
|
192
|
+
because the bar outlives whatever produced it.
|
|
193
|
+
]]
|
|
194
|
+
title: string,
|
|
156
195
|
}
|
|
157
196
|
|
|
158
197
|
type Runtime = {
|
|
@@ -174,6 +213,20 @@ type Runtime = {
|
|
|
174
213
|
|
|
175
214
|
energy: number,
|
|
176
215
|
shake: number,
|
|
216
|
+
--[[
|
|
217
|
+
The solid's size, as a spring rather than a value.
|
|
218
|
+
|
|
219
|
+
Scale was previously a small function of energy -- a 24% range that
|
|
220
|
+
nobody could see. Driving it as a spring means each event can push it and
|
|
221
|
+
let physics do the rest: a dispatch pulls it in, a reply throws it out
|
|
222
|
+
past its resting size, and it settles on its own.
|
|
223
|
+
]]
|
|
224
|
+
scale: number,
|
|
225
|
+
scaleVelocity: number,
|
|
226
|
+
-- Brightens and fattens the vertices for a moment after a call lands.
|
|
227
|
+
flare: number,
|
|
228
|
+
-- Seconds since anything last ran. Drives the wind-down.
|
|
229
|
+
quiet: number,
|
|
177
230
|
tint: Color3,
|
|
178
231
|
targetTint: Color3,
|
|
179
232
|
baseTint: Color3,
|
|
@@ -200,6 +253,10 @@ local runtime: Runtime = {
|
|
|
200
253
|
shapeIndex = 1,
|
|
201
254
|
energy = 0,
|
|
202
255
|
shake = 0,
|
|
256
|
+
scale = 1,
|
|
257
|
+
scaleVelocity = 0,
|
|
258
|
+
flare = 0,
|
|
259
|
+
quiet = 0,
|
|
203
260
|
tint = Color3.fromRGB(167, 139, 250),
|
|
204
261
|
targetTint = Color3.fromRGB(167, 139, 250),
|
|
205
262
|
baseTint = Color3.fromRGB(167, 139, 250),
|
|
@@ -251,7 +308,16 @@ local function step(delta: number)
|
|
|
251
308
|
|
|
252
309
|
runtime.energy = math.max(0, runtime.energy - delta * 0.9)
|
|
253
310
|
runtime.shake = math.max(0, runtime.shake - delta * 2.4)
|
|
254
|
-
|
|
311
|
+
--[[
|
|
312
|
+
Slower than it was. The shape is meant to climb as the session works, and
|
|
313
|
+
at the old rates a call had to arrive every three seconds just to hold the
|
|
314
|
+
octahedron -- so the icosahedron at the top of the list was, in practice,
|
|
315
|
+
unreachable. A burst of four calls now gets there, and it unwinds over
|
|
316
|
+
about ten seconds afterwards.
|
|
317
|
+
]]
|
|
318
|
+
runtime.activity = math.max(0, runtime.activity - delta * 0.28)
|
|
319
|
+
runtime.flare = math.max(0, runtime.flare - delta * 3)
|
|
320
|
+
runtime.quiet += delta
|
|
255
321
|
--[[
|
|
256
322
|
Two colours, blended by how recently something happened.
|
|
257
323
|
|
|
@@ -268,14 +334,52 @@ local function step(delta: number)
|
|
|
268
334
|
|
|
269
335
|
-- Turning faster while busy makes the rate itself readable: a glance says
|
|
270
336
|
-- whether anything is happening without reading a word.
|
|
271
|
-
|
|
337
|
+
--
|
|
338
|
+
-- And nearly stopping once the session has gone quiet, for the same reason in
|
|
339
|
+
-- reverse: a strip that keeps spinning at its working rate hours after the
|
|
340
|
+
-- last command claims an activity that is not happening.
|
|
341
|
+
local pace = if runtime.quiet > QUIET_AFTER then 0.25 else 1
|
|
342
|
+
runtime.angle += runtime.spin * delta * (1 + runtime.energy * 2.2) * pace
|
|
343
|
+
|
|
344
|
+
--[[
|
|
345
|
+
Where the solid wants to be, before the spring gets a say.
|
|
346
|
+
|
|
347
|
+
Three things move it and they are deliberately ordered. Energy -- how
|
|
348
|
+
recently work happened -- swells it while a session is busy. The breathe
|
|
349
|
+
is a slow sine that only survives while energy is near zero, so an idle
|
|
350
|
+
panel is visibly alive without a resting animation competing with the
|
|
351
|
+
reaction to a real call. And past QUIET_AFTER seconds of nothing, it draws
|
|
352
|
+
in and stays there: the picture of a session that has stopped.
|
|
353
|
+
]]
|
|
354
|
+
local calm = 1 - math.clamp(runtime.energy, 0, 1)
|
|
355
|
+
local breathe = math.sin(os.clock() * 0.9) * 0.04 * calm
|
|
356
|
+
local target = 1 + math.clamp(runtime.energy, 0, 1.6) * 0.18 + breathe
|
|
357
|
+
if runtime.quiet > QUIET_AFTER then
|
|
358
|
+
target = 0.82
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
--[[
|
|
362
|
+
A damped spring rather than a lerp, because a lerp cannot overshoot and
|
|
363
|
+
overshoot is the whole point: a reply that pushes the solid past its
|
|
364
|
+
resting size and lets it fall back reads as a thing being struck, where
|
|
365
|
+
easing toward a value reads as a slider being dragged.
|
|
366
|
+
|
|
367
|
+
Integrated semi-implicitly -- velocity first, then position from the new
|
|
368
|
+
velocity -- which stays stable at the frame times Studio actually hands us
|
|
369
|
+
rather than only at small ones.
|
|
370
|
+
]]
|
|
371
|
+
local step = math.min(delta, 1 / 30)
|
|
372
|
+
runtime.scaleVelocity += (target - runtime.scale) * SPRING * step
|
|
373
|
+
runtime.scaleVelocity -= runtime.scaleVelocity * DAMPING * step
|
|
374
|
+
runtime.scale += runtime.scaleVelocity * step
|
|
375
|
+
runtime.scale = math.clamp(runtime.scale, 0.55, 1.7)
|
|
272
376
|
|
|
273
377
|
local shape = SHAPES[runtime.shapeIndex]
|
|
274
378
|
local centre = Vector2.new(CELL / 2, CELL / 2)
|
|
275
379
|
-- Kept clear of the cell's edge. The perspective divide pushes a near vertex
|
|
276
380
|
-- outward, so sizing to the full half-width clipped the solid against its own
|
|
277
381
|
-- container whenever a corner swung toward the viewer.
|
|
278
|
-
local radius = CELL *
|
|
382
|
+
local radius = CELL * BASE_RADIUS * runtime.scale
|
|
279
383
|
|
|
280
384
|
-- Wobble the axis rather than the panel: shaking the strip would read as a
|
|
281
385
|
-- glitch, tilting the solid reads as it being knocked.
|
|
@@ -309,9 +413,9 @@ local function step(delta: number)
|
|
|
309
413
|
frame,
|
|
310
414
|
projected[a],
|
|
311
415
|
projected[b],
|
|
312
|
-
math.max(1, math.floor(1 + depth * 1.6)),
|
|
313
|
-
runtime.tint:Lerp(Color3.new(1, 1, 1), depth * 0.35),
|
|
314
|
-
0.84 - depth * 0.74
|
|
416
|
+
math.max(1, math.floor(1 + depth * 1.6 + runtime.flare * 0.8)),
|
|
417
|
+
runtime.tint:Lerp(Color3.new(1, 1, 1), depth * 0.35 + runtime.flare * 0.3),
|
|
418
|
+
math.max(0, 0.84 - depth * 0.74 - runtime.flare * 0.3)
|
|
315
419
|
)
|
|
316
420
|
end
|
|
317
421
|
|
|
@@ -330,12 +434,15 @@ local function step(delta: number)
|
|
|
330
434
|
continue
|
|
331
435
|
end
|
|
332
436
|
local depth = math.clamp((depths[index] + 2) / 4, 0, 1)
|
|
333
|
-
|
|
437
|
+
-- The corners carry the flare. A pulse of light travelling through the
|
|
438
|
+
-- vertices is what makes a completed call land as an event rather than as
|
|
439
|
+
-- a bar quietly appearing somewhere off to the right.
|
|
440
|
+
local size = math.max(2, math.floor(1.5 + depth * 2.5 + runtime.flare * 2.5))
|
|
334
441
|
dot.Visible = true
|
|
335
442
|
dot.Position = UDim2.fromOffset(point.X, point.Y)
|
|
336
443
|
dot.Size = UDim2.fromOffset(size, size)
|
|
337
444
|
dot.BackgroundColor3 = runtime.tint:Lerp(Color3.new(1, 1, 1), 0.25 + depth * 0.5)
|
|
338
|
-
dot.BackgroundTransparency = 0.55 - depth * 0.5
|
|
445
|
+
dot.BackgroundTransparency = math.max(0, 0.55 - depth * 0.5 - runtime.flare * 0.45)
|
|
339
446
|
end
|
|
340
447
|
|
|
341
448
|
--[[
|
|
@@ -392,12 +499,29 @@ local function showReading(entry: Bar)
|
|
|
392
499
|
return
|
|
393
500
|
end
|
|
394
501
|
|
|
395
|
-
|
|
502
|
+
--[[
|
|
503
|
+
The Y here has to be 1, and was 0.
|
|
504
|
+
|
|
505
|
+
The highlight is anchored to its own bottom edge, so a Y scale of 0 put
|
|
506
|
+
that edge on the trace's top line and drew the whole 26px column above it
|
|
507
|
+
-- outside a frame that clips its descendants. The hover has therefore
|
|
508
|
+
never been visible to anyone: the hit targets fired, the readout appeared,
|
|
509
|
+
and the highlight they were meant to explain was off screen every time.
|
|
510
|
+
]]
|
|
511
|
+
highlight.Position = UDim2.new(entry.hit.Position.X.Scale, entry.hit.Position.X.Offset, 1, 0)
|
|
396
512
|
highlight.Visible = true
|
|
397
513
|
|
|
514
|
+
--[[
|
|
515
|
+
Named, not just timed.
|
|
516
|
+
|
|
517
|
+
A duration with nothing attached to it raises the question it cannot
|
|
518
|
+
answer -- forty bars, one of them tall, and no way to tell whether that
|
|
519
|
+
was a script edit or a screenshot. The title is the same phrase the log
|
|
520
|
+
row uses, so pointing at a bar and reading the log agree with each other.
|
|
521
|
+
]]
|
|
398
522
|
readout.Text = if entry.ok
|
|
399
|
-
then string.format("%d ms", math.round(entry.milliseconds))
|
|
400
|
-
else string.format("failed after %d ms", math.round(entry.milliseconds))
|
|
523
|
+
then string.format("%s: %d ms", entry.title, math.round(entry.milliseconds))
|
|
524
|
+
else string.format("%s: failed after %d ms", entry.title, math.round(entry.milliseconds))
|
|
401
525
|
readout.TextColor3 = if entry.ok
|
|
402
526
|
then runtime.palette.text or Color3.fromRGB(226, 232, 240)
|
|
403
527
|
else runtime.palette.red or Color3.fromRGB(251, 113, 133)
|
|
@@ -420,11 +544,26 @@ end
|
|
|
420
544
|
distinguishable at a glance -- which is the pair of questions that actually
|
|
421
545
|
get asked of a log this size.
|
|
422
546
|
]]
|
|
423
|
-
function Visuals.recordCall(milliseconds: number, ok: boolean)
|
|
547
|
+
function Visuals.recordCall(milliseconds: number, ok: boolean, title: string)
|
|
424
548
|
runtime.energy = math.min(1.6, runtime.energy + (if ok then 0.3 else 0.8))
|
|
425
|
-
runtime.activity = math.min(1, runtime.activity + 0.
|
|
549
|
+
runtime.activity = math.min(1, runtime.activity + 0.34)
|
|
426
550
|
runtime.shapeIndex = math.clamp(1 + math.floor(runtime.activity * (#SHAPES - 0.001)), 1, #SHAPES)
|
|
427
|
-
|
|
551
|
+
runtime.quiet = 0
|
|
552
|
+
runtime.flare = 1
|
|
553
|
+
|
|
554
|
+
--[[
|
|
555
|
+
A shove rather than a new target.
|
|
556
|
+
|
|
557
|
+
Setting the scale outright would snap; giving the spring velocity lets it
|
|
558
|
+
carry past its resting size and fall back, which is the difference between
|
|
559
|
+
a value changing and something being struck. A failure shoves the other
|
|
560
|
+
way and knocks the axis with it.
|
|
561
|
+
]]
|
|
562
|
+
if ok then
|
|
563
|
+
runtime.scaleVelocity += 9
|
|
564
|
+
else
|
|
565
|
+
runtime.scale = 0.8
|
|
566
|
+
runtime.scaleVelocity = -2
|
|
428
567
|
runtime.shake = 1
|
|
429
568
|
end
|
|
430
569
|
|
|
@@ -449,7 +588,13 @@ function Visuals.recordCall(milliseconds: number, ok: boolean)
|
|
|
449
588
|
hit.Size = UDim2.new(0, BAR_STRIDE, 1, 0)
|
|
450
589
|
hit.Parent = trace
|
|
451
590
|
|
|
452
|
-
local entry: Bar = {
|
|
591
|
+
local entry: Bar = {
|
|
592
|
+
frame = bar,
|
|
593
|
+
hit = hit,
|
|
594
|
+
milliseconds = milliseconds,
|
|
595
|
+
ok = ok,
|
|
596
|
+
title = if title ~= "" then title else "call",
|
|
597
|
+
}
|
|
453
598
|
table.insert(runtime.bars, entry)
|
|
454
599
|
|
|
455
600
|
hit.MouseEnter:Connect(function()
|
|
@@ -507,6 +652,15 @@ end
|
|
|
507
652
|
function Visuals.setKind(color: Color3, urgency: number)
|
|
508
653
|
runtime.activeTint = color
|
|
509
654
|
runtime.energy = math.min(1.6, math.max(runtime.energy, urgency))
|
|
655
|
+
runtime.quiet = 0
|
|
656
|
+
--[[
|
|
657
|
+
Drawn in as the command goes out, so the pair reads as one gesture: the
|
|
658
|
+
solid contracts while the request is in flight and springs open when the
|
|
659
|
+
answer arrives. On a fast call the two are almost one motion, which is
|
|
660
|
+
itself the report -- a slow call visibly holds its breath.
|
|
661
|
+
]]
|
|
662
|
+
runtime.scale = math.min(runtime.scale, 0.85)
|
|
663
|
+
runtime.scaleVelocity = math.min(runtime.scaleVelocity, 0)
|
|
510
664
|
-- Re-aimed per kind so successive commands of different types visibly
|
|
511
665
|
-- change the axis rather than continuing the same turn.
|
|
512
666
|
runtime.spin = Vector3.new(
|
|
@@ -543,6 +697,37 @@ function Visuals.setIdle()
|
|
|
543
697
|
else "waiting for a command"
|
|
544
698
|
end
|
|
545
699
|
|
|
700
|
+
--[[
|
|
701
|
+
Winds the solid down, because the session has stopped rather than paused.
|
|
702
|
+
|
|
703
|
+
The strip already decays on its own, but decay bottoms out at "idle and
|
|
704
|
+
turning", which looks the same after twenty seconds as after two hours. This
|
|
705
|
+
is the console telling the picture what it has just told the log, so the two
|
|
706
|
+
agree: the shape falls back to a tetrahedron, the spin drops to a quarter
|
|
707
|
+
pace, and the solid draws in.
|
|
708
|
+
]]
|
|
709
|
+
function Visuals.setQuiet()
|
|
710
|
+
runtime.quiet = QUIET_AFTER + 1
|
|
711
|
+
runtime.activity = 0
|
|
712
|
+
runtime.shapeIndex = 1
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
--[[
|
|
716
|
+
Empties the trace.
|
|
717
|
+
|
|
718
|
+
Paired with the console's clear button. The bars used to survive it while the
|
|
719
|
+
footer counters reset, so "clear" wiped the log, wiped the statistics, and
|
|
720
|
+
left forty timings from the session it had just erased sitting on screen.
|
|
721
|
+
]]
|
|
722
|
+
function Visuals.clearTrace()
|
|
723
|
+
for _, entry in runtime.bars do
|
|
724
|
+
entry.frame:Destroy()
|
|
725
|
+
entry.hit:Destroy()
|
|
726
|
+
end
|
|
727
|
+
table.clear(runtime.bars)
|
|
728
|
+
clearReading()
|
|
729
|
+
end
|
|
730
|
+
|
|
546
731
|
function Visuals.setVisible(visible: boolean)
|
|
547
732
|
local band = runtime.band
|
|
548
733
|
if band == nil then
|
|
@@ -649,7 +834,7 @@ function Visuals.mount(parent: Instance, palette: { [string]: Color3 })
|
|
|
649
834
|
local trace = Instance.new("Frame")
|
|
650
835
|
trace.Name = "Trace"
|
|
651
836
|
trace.Position = UDim2.new(0, CELL + GUTTER, 0, 21)
|
|
652
|
-
trace.Size = UDim2.new(1, -CELL - GUTTER - 12, 0,
|
|
837
|
+
trace.Size = UDim2.new(1, -CELL - GUTTER - 12, 0, TRACE_HEIGHT)
|
|
653
838
|
trace.BackgroundTransparency = 1
|
|
654
839
|
trace.ClipsDescendants = true
|
|
655
840
|
trace.Parent = band
|
|
@@ -677,7 +862,10 @@ function Visuals.mount(parent: Instance, palette: { [string]: Color3 })
|
|
|
677
862
|
highlight.Position = UDim2.new(1, 0, 1, 0)
|
|
678
863
|
highlight.Size = UDim2.new(0, BAR_STRIDE, 1, 0)
|
|
679
864
|
highlight.BackgroundColor3 = palette.text or Color3.fromRGB(226, 232, 240)
|
|
680
|
-
|
|
865
|
+
-- Faint enough not to hide the bar it sits behind, solid enough to be seen
|
|
866
|
+
-- at all. The previous value was chosen for a highlight that never rendered,
|
|
867
|
+
-- so it had never been looked at.
|
|
868
|
+
highlight.BackgroundTransparency = 0.7
|
|
681
869
|
highlight.BorderSizePixel = 0
|
|
682
870
|
highlight.Visible = false
|
|
683
871
|
highlight.Parent = trace
|
|
@@ -693,7 +881,11 @@ function Visuals.mount(parent: Instance, palette: { [string]: Color3 })
|
|
|
693
881
|
readout.Name = "Readout"
|
|
694
882
|
readout.AnchorPoint = Vector2.new(0, 0.5)
|
|
695
883
|
readout.Position = UDim2.new(0, 0, 0.5, 0)
|
|
696
|
-
|
|
884
|
+
-- Sized by its text now that it carries a name as well as a number. A fixed
|
|
885
|
+
-- 110px was enough for "18 ms" and truncates anything with a phrase in front
|
|
886
|
+
-- of it, which is the half worth reading.
|
|
887
|
+
readout.AutomaticSize = Enum.AutomaticSize.X
|
|
888
|
+
readout.Size = UDim2.new(0, 0, 0, 14)
|
|
697
889
|
readout.BackgroundColor3 = palette.background or Color3.fromRGB(11, 12, 20)
|
|
698
890
|
readout.BackgroundTransparency = 0.15
|
|
699
891
|
readout.BorderSizePixel = 0
|
|
@@ -705,6 +897,11 @@ function Visuals.mount(parent: Instance, palette: { [string]: Color3 })
|
|
|
705
897
|
readout.ZIndex = 3
|
|
706
898
|
readout.Parent = trace
|
|
707
899
|
runtime.readout = readout
|
|
900
|
+
|
|
901
|
+
local readoutPadding = Instance.new("UIPadding")
|
|
902
|
+
readoutPadding.PaddingLeft = UDim.new(0, 4)
|
|
903
|
+
readoutPadding.PaddingRight = UDim.new(0, 4)
|
|
904
|
+
readoutPadding.Parent = readout
|
|
708
905
|
end
|
|
709
906
|
|
|
710
907
|
return Visuals
|
|
@@ -84,6 +84,35 @@ local overflow = 0
|
|
|
84
84
|
local installed = false
|
|
85
85
|
local installError: string? = nil
|
|
86
86
|
|
|
87
|
+
--[[
|
|
88
|
+
Which lines currently carry a breakpoint, per script.
|
|
89
|
+
|
|
90
|
+
`ScriptDebuggerService` exposes no way to list what is set -- `AddBreakpoint`
|
|
91
|
+
and `RemoveBreakpoint` are the whole surface, one line at a time -- so
|
|
92
|
+
"clear every breakpoint in this script" needs its own record of what this
|
|
93
|
+
module put there, or it cannot be done at all without either the caller
|
|
94
|
+
naming every line back or this file falling back to `ClearBreakpoints()`,
|
|
95
|
+
which takes the whole session's breakpoints with it, including ones set by
|
|
96
|
+
someone else sharing this Studio.
|
|
97
|
+
]]
|
|
98
|
+
local trackedBreakpoints: { [Instance]: { [number]: boolean } } = {}
|
|
99
|
+
|
|
100
|
+
local function track(target: Instance, line: number)
|
|
101
|
+
local lines = trackedBreakpoints[target]
|
|
102
|
+
if lines == nil then
|
|
103
|
+
lines = {}
|
|
104
|
+
trackedBreakpoints[target] = lines
|
|
105
|
+
end
|
|
106
|
+
lines[line] = true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
local function untrack(target: Instance, line: number)
|
|
110
|
+
local lines = trackedBreakpoints[target]
|
|
111
|
+
if lines then
|
|
112
|
+
lines[line] = nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
87
116
|
--[[
|
|
88
117
|
Flattens whatever the service hands back.
|
|
89
118
|
|
|
@@ -309,6 +338,7 @@ function Debug.set(params: { [string]: any }): { [string]: any }
|
|
|
309
338
|
end)
|
|
310
339
|
|
|
311
340
|
if ok then
|
|
341
|
+
track(target, descriptor.Line)
|
|
312
342
|
table.insert(added, {
|
|
313
343
|
path = Paths.of(target),
|
|
314
344
|
line = descriptor.Line,
|
|
@@ -362,13 +392,39 @@ function Debug.clear(params: { [string]: any }): { [string]: any }
|
|
|
362
392
|
if typeof(path) == "string" and path ~= "" then
|
|
363
393
|
local target = Paths.resolve(path)
|
|
364
394
|
local line = tonumber(params.line)
|
|
365
|
-
|
|
366
|
-
|
|
395
|
+
|
|
396
|
+
if line ~= nil then
|
|
397
|
+
local ok, removed = pcall(function()
|
|
398
|
+
return service:RemoveBreakpoint(target, line)
|
|
399
|
+
end)
|
|
400
|
+
untrack(target, line)
|
|
401
|
+
return { removed = ok and removed == true, path = Paths.of(target), line = line }
|
|
367
402
|
end
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
403
|
+
|
|
404
|
+
--[[
|
|
405
|
+
`path` alone, no `line`: every breakpoint this module put in that
|
|
406
|
+
script. `ScriptDebuggerService` has no "list breakpoints in this
|
|
407
|
+
script" of its own -- only `AddBreakpoint`/`RemoveBreakpoint`, one
|
|
408
|
+
line at a time -- so `trackedBreakpoints` is what makes this
|
|
409
|
+
possible at all, and it is also the reason it can only remove what
|
|
410
|
+
this session set: a breakpoint another client or the user placed
|
|
411
|
+
by hand was never tracked here, and this cannot see it to touch it.
|
|
412
|
+
]]
|
|
413
|
+
local lines = trackedBreakpoints[target]
|
|
414
|
+
local removedLines: { number } = {}
|
|
415
|
+
if lines then
|
|
416
|
+
for lineNumber in lines do
|
|
417
|
+
local ok = pcall(function()
|
|
418
|
+
service:RemoveBreakpoint(target, lineNumber)
|
|
419
|
+
end)
|
|
420
|
+
if ok then
|
|
421
|
+
table.insert(removedLines, lineNumber)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
trackedBreakpoints[target] = nil
|
|
425
|
+
end
|
|
426
|
+
table.sort(removedLines)
|
|
427
|
+
return { removed = #removedLines > 0, path = Paths.of(target), lines = removedLines }
|
|
372
428
|
end
|
|
373
429
|
|
|
374
430
|
local ok, err = pcall(function()
|
|
@@ -377,6 +433,7 @@ function Debug.clear(params: { [string]: any }): { [string]: any }
|
|
|
377
433
|
if not ok then
|
|
378
434
|
Dispatch.fail("REFUSED", string.format("ClearBreakpoints refused: %s", tostring(err)))
|
|
379
435
|
end
|
|
436
|
+
table.clear(trackedBreakpoints)
|
|
380
437
|
return { cleared = true }
|
|
381
438
|
end
|
|
382
439
|
|
|
@@ -138,7 +138,6 @@ function Geometry.combine(params: { [string]: any }): { [string]: any }
|
|
|
138
138
|
then (if #produced == 1 then params.name else string.format("%s%d", params.name, index))
|
|
139
139
|
else subject.Name
|
|
140
140
|
part.Parent = parent
|
|
141
|
-
table.insert(created, Paths.of(part))
|
|
142
141
|
end
|
|
143
142
|
|
|
144
143
|
-- Removing the inputs is opt-in, and only once the results exist: an
|
|
@@ -150,6 +149,26 @@ function Geometry.combine(params: { [string]: any }): { [string]: any }
|
|
|
150
149
|
part:Destroy()
|
|
151
150
|
end
|
|
152
151
|
end
|
|
152
|
+
|
|
153
|
+
--[[
|
|
154
|
+
Paths for the produced parts are read only now, after the originals
|
|
155
|
+
are gone rather than in the loop above that named and parented them.
|
|
156
|
+
|
|
157
|
+
A result keeps the subject's name by default, so for the entire
|
|
158
|
+
window before the destroy above runs, the new part and the subject
|
|
159
|
+
it was built from are two same-named siblings in the same parent --
|
|
160
|
+
and Paths.of correctly, but uselessly, disambiguates that with a
|
|
161
|
+
`[2]` suffix that stops being true the instant the destroy runs a
|
|
162
|
+
few lines later. The caller reads this path after the call returns,
|
|
163
|
+
by which point the disambiguation was already wrong. Reading it here
|
|
164
|
+
instead describes where the part actually is once nothing further
|
|
165
|
+
in this function is going to move or remove anything -- correct
|
|
166
|
+
whether or not `keepOriginals` left a real, permanent collision for
|
|
167
|
+
it to still be disambiguating.
|
|
168
|
+
]]
|
|
169
|
+
for _, part in produced do
|
|
170
|
+
table.insert(created, Paths.of(part))
|
|
171
|
+
end
|
|
153
172
|
end)
|
|
154
173
|
|
|
155
174
|
return { created = created, removed = removed, undoable = undoable }
|
|
@@ -29,6 +29,7 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
|
29
29
|
local RunService = game:GetService("RunService")
|
|
30
30
|
|
|
31
31
|
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
32
|
+
local Emulation = require(script.Parent.Parent.Emulation)
|
|
32
33
|
|
|
33
34
|
local Input = {}
|
|
34
35
|
|
|
@@ -73,6 +74,36 @@ end
|
|
|
73
74
|
local virtual = UserInputService:CreateVirtualInput()
|
|
74
75
|
local performed = {}
|
|
75
76
|
|
|
77
|
+
--[[
|
|
78
|
+
Where the client actually read the last pointer event, against where it was
|
|
79
|
+
aimed.
|
|
80
|
+
|
|
81
|
+
Measured, because it cannot be derived. A click sent at (300,300) with a
|
|
82
|
+
Galaxy S25 Ultra emulated is read at (253,242); the same click with an
|
|
83
|
+
iPhone 16 in portrait is read at (300,183). Constant per configuration, and
|
|
84
|
+
fitting no formula over device resolution, viewport size and GUI inset that
|
|
85
|
+
holds for both -- the horizontal term matches (device - viewport) / 2 and
|
|
86
|
+
the vertical term does not.
|
|
87
|
+
|
|
88
|
+
So it is reported rather than predicted. The caller aims once, sees where it
|
|
89
|
+
landed, and corrects; that works on every device, in both orientations, and
|
|
90
|
+
with no device emulated at all, which no formula here has managed.
|
|
91
|
+
|
|
92
|
+
`InputBegan` rather than `GetMouseLocation`: the latter is frozen at the
|
|
93
|
+
last real click under touch emulation -- twenty samples across two sent
|
|
94
|
+
moves never changed it -- and is offset from `InputBegan.Position` by the
|
|
95
|
+
GUI inset besides. Phone emulation also turns MouseEnabled off and delivers
|
|
96
|
+
these as Touch, so both types are watched.
|
|
97
|
+
]]
|
|
98
|
+
local landed = nil
|
|
99
|
+
local aimed = nil
|
|
100
|
+
UserInputService.InputBegan:Connect(function(input)
|
|
101
|
+
local kind = input.UserInputType
|
|
102
|
+
if aimed ~= nil and (kind == Enum.UserInputType.MouseButton1 or kind == Enum.UserInputType.Touch) then
|
|
103
|
+
landed = { sent = aimed, seen = { x = input.Position.X, y = input.Position.Y } }
|
|
104
|
+
end
|
|
105
|
+
end)
|
|
106
|
+
|
|
76
107
|
--[[
|
|
77
108
|
The on-screen pointer.
|
|
78
109
|
|
|
@@ -240,6 +271,7 @@ for _, step in plan do
|
|
|
240
271
|
elseif kind == "click" then
|
|
241
272
|
local button = Enum.UserInputType[step.button or "MouseButton1"]
|
|
242
273
|
local at = Vector2.new(step.x, step.y)
|
|
274
|
+
aimed = { x = step.x, y = step.y }
|
|
243
275
|
-- Travel first, then click. Teleporting the pointer onto the target at
|
|
244
276
|
-- the same instant it fires looks like nothing happened at all.
|
|
245
277
|
moveCursor(step.x, step.y, 0.3)
|
|
@@ -265,7 +297,7 @@ if cursorGui ~= nil then
|
|
|
265
297
|
cursorGui:Destroy()
|
|
266
298
|
end
|
|
267
299
|
|
|
268
|
-
report:FireServer({ ok = true, performed = performed })
|
|
300
|
+
report:FireServer({ ok = true, performed = performed, landed = landed })
|
|
269
301
|
]==]
|
|
270
302
|
|
|
271
303
|
local function playerFor(name: string?): Player
|
|
@@ -441,6 +473,14 @@ function Input.send(params: { [string]: any }): { [string]: any }
|
|
|
441
473
|
steps = #plan,
|
|
442
474
|
player = player.Name,
|
|
443
475
|
performed = result.performed,
|
|
476
|
+
--[[
|
|
477
|
+
Named so the caller can tell why a click that was delivered still hit
|
|
478
|
+
nothing. `landed` carries the arithmetic; this says what to turn off.
|
|
479
|
+
]]
|
|
480
|
+
emulation = Emulation.summary(),
|
|
481
|
+
-- Where the client read the last click, against where it was aimed. See
|
|
482
|
+
-- the note in RELAY_SOURCE for why this is measured and not computed.
|
|
483
|
+
landed = result.landed,
|
|
444
484
|
}
|
|
445
485
|
end
|
|
446
486
|
|