@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
|
@@ -1,230 +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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
local
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
package/plugin/src/Undo.luau
CHANGED
|
@@ -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
|
|
package/plugin/src/Visuals.luau
CHANGED
|
@@ -270,11 +270,49 @@ local function step(delta: number)
|
|
|
270
270
|
velocity -- which stays stable at the frame times Studio actually hands us
|
|
271
271
|
rather than only at small ones.
|
|
272
272
|
]]
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
--[[
|
|
274
|
+
Substepped rather than truncated.
|
|
275
|
+
|
|
276
|
+
`math.min(delta, 1/30)` kept the integration stable, but it also THREW
|
|
277
|
+
AWAY the rest of a long frame: at 20fps a third of every frame's motion
|
|
278
|
+
simply did not happen, so the spring ran slower in wall-clock time
|
|
279
|
+
exactly when Studio was busiest -- which is during a tool call, which is
|
|
280
|
+
when the spring is reacting to something. Stepping the leftover instead
|
|
281
|
+
keeps the reaction the same length whatever the frame rate, and the cap
|
|
282
|
+
on the number of steps stops a two-second hitch from paying it all back
|
|
283
|
+
in one frame.
|
|
284
|
+
]]
|
|
285
|
+
local remaining = math.min(delta, 0.25)
|
|
286
|
+
for _ = 1, 8 do
|
|
287
|
+
if remaining <= 0 then
|
|
288
|
+
break
|
|
289
|
+
end
|
|
290
|
+
local integration = math.min(remaining, 1 / 60)
|
|
291
|
+
remaining -= integration
|
|
292
|
+
|
|
293
|
+
runtime.scaleVelocity += (target - runtime.scale) * SPRING * integration
|
|
294
|
+
runtime.scaleVelocity -= runtime.scaleVelocity * DAMPING * integration
|
|
295
|
+
runtime.scale += runtime.scaleVelocity * integration
|
|
296
|
+
|
|
297
|
+
--[[
|
|
298
|
+
Clamping the position without killing the velocity is what made a
|
|
299
|
+
reply read as a jolt rather than a bounce. The reply impulse is
|
|
300
|
+
`scaleVelocity += 9`, which crosses the whole 0.55..1.7 range in
|
|
301
|
+
about two frames, so the scale pinned itself against the ceiling
|
|
302
|
+
while still carrying all that speed -- and then sat there, motionless
|
|
303
|
+
but not at rest, until the spring had spent the stored velocity. The
|
|
304
|
+
eye reads that as a snap, a stall, and a second snap.
|
|
305
|
+
|
|
306
|
+
Zeroing the component pushing into the rail turns it into a stop.
|
|
307
|
+
]]
|
|
308
|
+
if runtime.scale <= 0.55 then
|
|
309
|
+
runtime.scale = 0.55
|
|
310
|
+
runtime.scaleVelocity = math.max(runtime.scaleVelocity, 0)
|
|
311
|
+
elseif runtime.scale >= 1.7 then
|
|
312
|
+
runtime.scale = 1.7
|
|
313
|
+
runtime.scaleVelocity = math.min(runtime.scaleVelocity, 0)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
278
316
|
|
|
279
317
|
--[[
|
|
280
318
|
Handed off. A preset that throws must not take the console's status
|
|
@@ -414,7 +452,19 @@ function Visuals.recordCall(milliseconds: number, ok: boolean, title: string)
|
|
|
414
452
|
way and knocks the axis with it.
|
|
415
453
|
]]
|
|
416
454
|
if ok then
|
|
417
|
-
|
|
455
|
+
--[[
|
|
456
|
+
Sized to peak just under the clamp rather than against it.
|
|
457
|
+
|
|
458
|
+
At +9 the shove carried the scale into the 1.7 ceiling within about a
|
|
459
|
+
tenth of a second, so what should have been an arc became a rise, a
|
|
460
|
+
flat spot, and a drop -- and a burst of calls, each adding another 9
|
|
461
|
+
to a velocity that was already railed, held it pinned there for as
|
|
462
|
+
long as the burst lasted. +6 tops out around 1.55, which reads as the
|
|
463
|
+
same strike and stays on the curve. The accumulated case is capped
|
|
464
|
+
for the same reason: several calls at once should look busy, not
|
|
465
|
+
stuck.
|
|
466
|
+
]]
|
|
467
|
+
runtime.scaleVelocity = math.min(runtime.scaleVelocity + 6, 8)
|
|
418
468
|
else
|
|
419
469
|
runtime.scale = 0.8
|
|
420
470
|
runtime.scaleVelocity = -2
|
|
@@ -68,9 +68,29 @@ function Device.set(params: { [string]: any }): { [string]: any }
|
|
|
68
68
|
(StudioDeviceSimulatorService :: any):SetDeviceAsync(id)
|
|
69
69
|
end)
|
|
70
70
|
if not ok then
|
|
71
|
+
--[[
|
|
72
|
+
Two different failures, and calling both NO_SUCH_DEVICE sends the
|
|
73
|
+
caller to check a spelling that was never wrong.
|
|
74
|
+
|
|
75
|
+
The simulator refuses outright while a playtest is running -- "cannot
|
|
76
|
+
change device in current game state" -- and this handler is reachable
|
|
77
|
+
from the playtest session, which is exactly where someone testing a
|
|
78
|
+
phone HUD would call it. The device is settable from the EDIT session
|
|
79
|
+
meanwhile, playtest or no playtest, so the fix is a sentence, not a
|
|
80
|
+
spelling check.
|
|
81
|
+
]]
|
|
82
|
+
local reason = tostring(err)
|
|
83
|
+
if string.find(reason, "game state", 1, true) ~= nil then
|
|
84
|
+
Dispatch.fail(
|
|
85
|
+
"DEVICE_LOCKED",
|
|
86
|
+
string.format("Studio will not switch to %q while a playtest is running.", id),
|
|
87
|
+
"Send this to the EDIT session's studioId instead -- it changes the viewport for "
|
|
88
|
+
.. "the running playtest too -- or set the device before pressing play."
|
|
89
|
+
)
|
|
90
|
+
end
|
|
71
91
|
Dispatch.fail(
|
|
72
92
|
"NO_SUCH_DEVICE",
|
|
73
|
-
string.format("Could not switch to %q: %s", id,
|
|
93
|
+
string.format("Could not switch to %q: %s", id, reason),
|
|
74
94
|
"Device ids look like \"iphone_16\" or \"ipad_a16\"; call `list` for the full set."
|
|
75
95
|
)
|
|
76
96
|
end
|