rbxstudio-mcp 1.9.0
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/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/__tests__/bridge-service.test.d.ts +2 -0
- package/dist/__tests__/bridge-service.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-service.test.js +109 -0
- package/dist/__tests__/bridge-service.test.js.map +1 -0
- package/dist/__tests__/http-server.test.d.ts +2 -0
- package/dist/__tests__/http-server.test.d.ts.map +1 -0
- package/dist/__tests__/http-server.test.js +193 -0
- package/dist/__tests__/http-server.test.js.map +1 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +182 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/smoke.test.d.ts +2 -0
- package/dist/__tests__/smoke.test.d.ts.map +1 -0
- package/dist/__tests__/smoke.test.js +63 -0
- package/dist/__tests__/smoke.test.js.map +1 -0
- package/dist/bridge-service.d.ts +17 -0
- package/dist/bridge-service.d.ts.map +1 -0
- package/dist/bridge-service.js +77 -0
- package/dist/bridge-service.js.map +1 -0
- package/dist/http-server.d.ts +4 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +290 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1102 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/index.d.ts +273 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +628 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/studio-client.d.ts +7 -0
- package/dist/tools/studio-client.d.ts.map +1 -0
- package/dist/tools/studio-client.js +19 -0
- package/dist/tools/studio-client.js.map +1 -0
- package/package.json +69 -0
- package/studio-plugin/INSTALLATION.md +150 -0
- package/studio-plugin/MCPPlugin.rbxmx +3253 -0
- package/studio-plugin/plugin.json +10 -0
- package/studio-plugin/plugin.luau +3584 -0
|
@@ -0,0 +1,3253 @@
|
|
|
1
|
+
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
|
|
2
|
+
<External>null</External>
|
|
3
|
+
<External>nil</External>
|
|
4
|
+
<Item class="Script" referent="RBX91DA8FC31CC84F2298F74E8208924E33">
|
|
5
|
+
<Properties>
|
|
6
|
+
<ProtectedString name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
|
|
7
|
+
local StudioService = game:GetService("StudioService")
|
|
8
|
+
local Selection = game:GetService("Selection")
|
|
9
|
+
local RunService = game:GetService("RunService")
|
|
10
|
+
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
11
|
+
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
12
|
+
local CollectionService = game:GetService("CollectionService")
|
|
13
|
+
|
|
14
|
+
local toolbar = plugin:CreateToolbar("MCP Integration")
|
|
15
|
+
local button =
|
|
16
|
+
toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration", "rbxassetid://10734944444")
|
|
17
|
+
|
|
18
|
+
local pluginState = {
|
|
19
|
+
serverUrl = "http://localhost:3002",
|
|
20
|
+
mcpServerUrl = "http://localhost:3001",
|
|
21
|
+
isActive = false,
|
|
22
|
+
pollInterval = 0.5,
|
|
23
|
+
lastPoll = 0,
|
|
24
|
+
consecutiveFailures = 0,
|
|
25
|
+
maxFailuresBeforeError = 50,
|
|
26
|
+
lastSuccessfulConnection = 0,
|
|
27
|
+
currentRetryDelay = 0.5,
|
|
28
|
+
maxRetryDelay = 5,
|
|
29
|
+
retryBackoffMultiplier = 1.2,
|
|
30
|
+
-- UI step tracking
|
|
31
|
+
lastHttpOk = false,
|
|
32
|
+
mcpWaitStartTime = nil,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
local screenGui = plugin:CreateDockWidgetPluginGuiAsync(
|
|
36
|
+
"MCPServerInterface",
|
|
37
|
+
DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 400, 500, 350, 450)
|
|
38
|
+
)
|
|
39
|
+
screenGui.Title = "MCP Server v1.8.0"
|
|
40
|
+
|
|
41
|
+
local mainFrame = Instance.new("Frame")
|
|
42
|
+
mainFrame.Size = UDim2.new(1, 0, 1, 0)
|
|
43
|
+
mainFrame.BackgroundColor3 = Color3.fromRGB(17, 24, 39)
|
|
44
|
+
mainFrame.BorderSizePixel = 0
|
|
45
|
+
mainFrame.Parent = screenGui
|
|
46
|
+
|
|
47
|
+
local mainCorner = Instance.new("UICorner")
|
|
48
|
+
mainCorner.CornerRadius = UDim.new(0, 8)
|
|
49
|
+
mainCorner.Parent = mainFrame
|
|
50
|
+
|
|
51
|
+
local headerFrame = Instance.new("Frame")
|
|
52
|
+
headerFrame.Size = UDim2.new(1, 0, 0, 60)
|
|
53
|
+
headerFrame.Position = UDim2.new(0, 0, 0, 0)
|
|
54
|
+
headerFrame.BackgroundColor3 = Color3.fromRGB(59, 130, 246)
|
|
55
|
+
headerFrame.BorderSizePixel = 0
|
|
56
|
+
headerFrame.Parent = mainFrame
|
|
57
|
+
|
|
58
|
+
local headerCorner = Instance.new("UICorner")
|
|
59
|
+
headerCorner.CornerRadius = UDim.new(0, 8)
|
|
60
|
+
headerCorner.Parent = headerFrame
|
|
61
|
+
|
|
62
|
+
local headerGradient = Instance.new("UIGradient")
|
|
63
|
+
headerGradient.Color = ColorSequence.new{
|
|
64
|
+
ColorSequenceKeypoint.new(0, Color3.fromRGB(59, 130, 246)),
|
|
65
|
+
ColorSequenceKeypoint.new(1, Color3.fromRGB(147, 51, 234))
|
|
66
|
+
}
|
|
67
|
+
headerGradient.Rotation = 45
|
|
68
|
+
headerGradient.Parent = headerFrame
|
|
69
|
+
|
|
70
|
+
local titleContainer = Instance.new("Frame")
|
|
71
|
+
titleContainer.Size = UDim2.new(1, -70, 1, 0)
|
|
72
|
+
titleContainer.Position = UDim2.new(0, 15, 0, 0)
|
|
73
|
+
titleContainer.BackgroundTransparency = 1
|
|
74
|
+
titleContainer.Parent = headerFrame
|
|
75
|
+
|
|
76
|
+
local titleLabel = Instance.new("TextLabel")
|
|
77
|
+
titleLabel.Size = UDim2.new(1, 0, 0, 28)
|
|
78
|
+
titleLabel.Position = UDim2.new(0, 0, 0, 8)
|
|
79
|
+
titleLabel.BackgroundTransparency = 1
|
|
80
|
+
titleLabel.Text = "MCP Server"
|
|
81
|
+
titleLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
82
|
+
titleLabel.TextScaled = false
|
|
83
|
+
titleLabel.TextSize = 18
|
|
84
|
+
titleLabel.Font = Enum.Font.Jura
|
|
85
|
+
titleLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
86
|
+
titleLabel.Parent = titleContainer
|
|
87
|
+
|
|
88
|
+
local versionLabel = Instance.new("TextLabel")
|
|
89
|
+
versionLabel.Size = UDim2.new(1, 0, 0, 16)
|
|
90
|
+
versionLabel.Position = UDim2.new(0, 0, 0, 32)
|
|
91
|
+
versionLabel.BackgroundTransparency = 1
|
|
92
|
+
versionLabel.Text = "AI Integration • v1.7.2"
|
|
93
|
+
versionLabel.TextColor3 = Color3.fromRGB(191, 219, 254)
|
|
94
|
+
versionLabel.TextScaled = false
|
|
95
|
+
versionLabel.TextSize = 12
|
|
96
|
+
versionLabel.Font = Enum.Font.Jura
|
|
97
|
+
versionLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
98
|
+
versionLabel.Parent = titleContainer
|
|
99
|
+
|
|
100
|
+
local statusContainer = Instance.new("Frame")
|
|
101
|
+
statusContainer.Size = UDim2.new(0, 50, 0, 40)
|
|
102
|
+
statusContainer.Position = UDim2.new(1, -60, 0, 10)
|
|
103
|
+
statusContainer.BackgroundTransparency = 1
|
|
104
|
+
statusContainer.Parent = headerFrame
|
|
105
|
+
|
|
106
|
+
local statusIndicator = Instance.new("Frame")
|
|
107
|
+
statusIndicator.Size = UDim2.new(0, 16, 0, 16)
|
|
108
|
+
statusIndicator.Position = UDim2.new(0.5, -8, 0, 5)
|
|
109
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
110
|
+
statusIndicator.BorderSizePixel = 0
|
|
111
|
+
statusIndicator.Parent = statusContainer
|
|
112
|
+
|
|
113
|
+
local statusCorner = Instance.new("UICorner")
|
|
114
|
+
statusCorner.CornerRadius = UDim.new(1, 0)
|
|
115
|
+
statusCorner.Parent = statusIndicator
|
|
116
|
+
|
|
117
|
+
local statusPulse = Instance.new("Frame")
|
|
118
|
+
statusPulse.Size = UDim2.new(0, 16, 0, 16)
|
|
119
|
+
statusPulse.Position = UDim2.new(0, 0, 0, 0)
|
|
120
|
+
statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
121
|
+
statusPulse.BackgroundTransparency = 0.7
|
|
122
|
+
statusPulse.BorderSizePixel = 0
|
|
123
|
+
statusPulse.Parent = statusIndicator
|
|
124
|
+
|
|
125
|
+
local pulseCorner = Instance.new("UICorner")
|
|
126
|
+
pulseCorner.CornerRadius = UDim.new(1, 0)
|
|
127
|
+
pulseCorner.Parent = statusPulse
|
|
128
|
+
|
|
129
|
+
local statusText = Instance.new("TextLabel")
|
|
130
|
+
statusText.Size = UDim2.new(0, 50, 0, 12)
|
|
131
|
+
statusText.Position = UDim2.new(0, 0, 0, 24)
|
|
132
|
+
statusText.BackgroundTransparency = 1
|
|
133
|
+
statusText.Text = "OFFLINE"
|
|
134
|
+
statusText.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
135
|
+
statusText.TextScaled = false
|
|
136
|
+
statusText.TextSize = 8
|
|
137
|
+
statusText.Font = Enum.Font.Jura
|
|
138
|
+
statusText.TextXAlignment = Enum.TextXAlignment.Center
|
|
139
|
+
statusText.Parent = statusContainer
|
|
140
|
+
|
|
141
|
+
local contentFrame = Instance.new("ScrollingFrame")
|
|
142
|
+
contentFrame.Size = UDim2.new(1, -20, 1, -80)
|
|
143
|
+
contentFrame.Position = UDim2.new(0, 10, 0, 70)
|
|
144
|
+
contentFrame.BackgroundTransparency = 1
|
|
145
|
+
contentFrame.BorderSizePixel = 0
|
|
146
|
+
contentFrame.ScrollBarThickness = 6
|
|
147
|
+
contentFrame.ScrollBarImageColor3 = Color3.fromRGB(99, 102, 241)
|
|
148
|
+
contentFrame.CanvasSize = UDim2.new(0, 0, 0, 243)
|
|
149
|
+
contentFrame.AutomaticCanvasSize = Enum.AutomaticSize.None
|
|
150
|
+
contentFrame.Parent = mainFrame
|
|
151
|
+
|
|
152
|
+
local contentLayout = Instance.new("UIListLayout")
|
|
153
|
+
contentLayout.Padding = UDim.new(0, 12)
|
|
154
|
+
contentLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
155
|
+
contentLayout.Parent = contentFrame
|
|
156
|
+
|
|
157
|
+
local connectionSection = Instance.new("Frame")
|
|
158
|
+
connectionSection.Size = UDim2.new(1, 0, 0, 110)
|
|
159
|
+
connectionSection.BackgroundColor3 = Color3.fromRGB(31, 41, 55)
|
|
160
|
+
connectionSection.BorderSizePixel = 0
|
|
161
|
+
connectionSection.LayoutOrder = 1
|
|
162
|
+
connectionSection.Parent = contentFrame
|
|
163
|
+
|
|
164
|
+
local connectionCorner = Instance.new("UICorner")
|
|
165
|
+
connectionCorner.CornerRadius = UDim.new(0, 8)
|
|
166
|
+
connectionCorner.Parent = connectionSection
|
|
167
|
+
|
|
168
|
+
local connectionPadding = Instance.new("UIPadding")
|
|
169
|
+
connectionPadding.PaddingLeft = UDim.new(0, 15)
|
|
170
|
+
connectionPadding.PaddingRight = UDim.new(0, 15)
|
|
171
|
+
connectionPadding.PaddingTop = UDim.new(0, 15)
|
|
172
|
+
connectionPadding.PaddingBottom = UDim.new(0, 15)
|
|
173
|
+
connectionPadding.Parent = connectionSection
|
|
174
|
+
|
|
175
|
+
local connectionTitle = Instance.new("TextLabel")
|
|
176
|
+
connectionTitle.Size = UDim2.new(1, 0, 0, 20)
|
|
177
|
+
connectionTitle.Position = UDim2.new(0, 0, 0, 0)
|
|
178
|
+
connectionTitle.BackgroundTransparency = 1
|
|
179
|
+
connectionTitle.Text = "Connection Settings"
|
|
180
|
+
connectionTitle.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
181
|
+
connectionTitle.TextScaled = false
|
|
182
|
+
connectionTitle.TextSize = 14
|
|
183
|
+
connectionTitle.Font = Enum.Font.Jura
|
|
184
|
+
connectionTitle.TextXAlignment = Enum.TextXAlignment.Left
|
|
185
|
+
connectionTitle.Parent = connectionSection
|
|
186
|
+
|
|
187
|
+
local urlLabel = Instance.new("TextLabel")
|
|
188
|
+
urlLabel.Size = UDim2.new(1, 0, 0, 16)
|
|
189
|
+
urlLabel.Position = UDim2.new(0, 0, 0, 30)
|
|
190
|
+
urlLabel.BackgroundTransparency = 1
|
|
191
|
+
urlLabel.Text = "Server URL"
|
|
192
|
+
urlLabel.TextColor3 = Color3.fromRGB(156, 163, 175)
|
|
193
|
+
urlLabel.TextScaled = false
|
|
194
|
+
urlLabel.TextSize = 12
|
|
195
|
+
urlLabel.Font = Enum.Font.Jura
|
|
196
|
+
urlLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
197
|
+
urlLabel.Parent = connectionSection
|
|
198
|
+
|
|
199
|
+
local urlInput = Instance.new("TextBox")
|
|
200
|
+
urlInput.Size = UDim2.new(1, 0, 0, 32)
|
|
201
|
+
urlInput.Position = UDim2.new(0, 0, 0, 50)
|
|
202
|
+
urlInput.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
|
|
203
|
+
urlInput.BorderSizePixel = 1
|
|
204
|
+
urlInput.BorderColor3 = Color3.fromRGB(99, 102, 241)
|
|
205
|
+
urlInput.Text = "http://localhost:3002"
|
|
206
|
+
urlInput.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
207
|
+
urlInput.TextScaled = false
|
|
208
|
+
urlInput.TextSize = 12
|
|
209
|
+
urlInput.Font = Enum.Font.Jura
|
|
210
|
+
urlInput.ClearTextOnFocus = false
|
|
211
|
+
urlInput.PlaceholderText = "Enter server URL..."
|
|
212
|
+
urlInput.PlaceholderColor3 = Color3.fromRGB(107, 114, 128)
|
|
213
|
+
urlInput.Parent = connectionSection
|
|
214
|
+
|
|
215
|
+
local urlCorner = Instance.new("UICorner")
|
|
216
|
+
urlCorner.CornerRadius = UDim.new(0, 6)
|
|
217
|
+
urlCorner.Parent = urlInput
|
|
218
|
+
|
|
219
|
+
local urlPadding = Instance.new("UIPadding")
|
|
220
|
+
urlPadding.PaddingLeft = UDim.new(0, 12)
|
|
221
|
+
urlPadding.PaddingRight = UDim.new(0, 12)
|
|
222
|
+
urlPadding.Parent = urlInput
|
|
223
|
+
|
|
224
|
+
local statusSection = Instance.new("Frame")
|
|
225
|
+
statusSection.Size = UDim2.new(1, 0, 0, 170)
|
|
226
|
+
statusSection.BackgroundColor3 = Color3.fromRGB(31, 41, 55)
|
|
227
|
+
statusSection.BorderSizePixel = 0
|
|
228
|
+
statusSection.LayoutOrder = 2
|
|
229
|
+
statusSection.Parent = contentFrame
|
|
230
|
+
|
|
231
|
+
local statusSectionCorner = Instance.new("UICorner")
|
|
232
|
+
statusSectionCorner.CornerRadius = UDim.new(0, 8)
|
|
233
|
+
statusSectionCorner.Parent = statusSection
|
|
234
|
+
|
|
235
|
+
local statusSectionPadding = Instance.new("UIPadding")
|
|
236
|
+
statusSectionPadding.PaddingLeft = UDim.new(0, 15)
|
|
237
|
+
statusSectionPadding.PaddingRight = UDim.new(0, 15)
|
|
238
|
+
statusSectionPadding.PaddingTop = UDim.new(0, 15)
|
|
239
|
+
statusSectionPadding.PaddingBottom = UDim.new(0, 15)
|
|
240
|
+
statusSectionPadding.Parent = statusSection
|
|
241
|
+
|
|
242
|
+
local statusTitle = Instance.new("TextLabel")
|
|
243
|
+
statusTitle.Size = UDim2.new(1, 0, 0, 20)
|
|
244
|
+
statusTitle.Position = UDim2.new(0, 0, 0, 0)
|
|
245
|
+
statusTitle.BackgroundTransparency = 1
|
|
246
|
+
statusTitle.Text = "Connection Status"
|
|
247
|
+
statusTitle.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
248
|
+
statusTitle.TextScaled = false
|
|
249
|
+
statusTitle.TextSize = 14
|
|
250
|
+
statusTitle.Font = Enum.Font.Jura
|
|
251
|
+
statusTitle.TextXAlignment = Enum.TextXAlignment.Left
|
|
252
|
+
statusTitle.Parent = statusSection
|
|
253
|
+
|
|
254
|
+
local statusLabel = Instance.new("TextLabel")
|
|
255
|
+
statusLabel.Size = UDim2.new(1, 0, 0, 20)
|
|
256
|
+
statusLabel.Position = UDim2.new(0, 0, 0, 30)
|
|
257
|
+
statusLabel.BackgroundTransparency = 1
|
|
258
|
+
statusLabel.Text = "Disconnected"
|
|
259
|
+
statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
260
|
+
statusLabel.TextScaled = false
|
|
261
|
+
statusLabel.TextSize = 13
|
|
262
|
+
statusLabel.Font = Enum.Font.Jura
|
|
263
|
+
statusLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
264
|
+
statusLabel.TextWrapped = true
|
|
265
|
+
statusLabel.Parent = statusSection
|
|
266
|
+
|
|
267
|
+
local detailStatusLabel = Instance.new("TextLabel")
|
|
268
|
+
detailStatusLabel.Size = UDim2.new(1, 0, 0, 12)
|
|
269
|
+
detailStatusLabel.Position = UDim2.new(0, 0, 0, 50)
|
|
270
|
+
detailStatusLabel.BackgroundTransparency = 1
|
|
271
|
+
detailStatusLabel.Text = "HTTP: X MCP: X"
|
|
272
|
+
detailStatusLabel.TextColor3 = Color3.fromRGB(156, 163, 175)
|
|
273
|
+
detailStatusLabel.TextScaled = false
|
|
274
|
+
detailStatusLabel.TextSize = 10
|
|
275
|
+
detailStatusLabel.Font = Enum.Font.Jura
|
|
276
|
+
detailStatusLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
277
|
+
detailStatusLabel.TextWrapped = true
|
|
278
|
+
detailStatusLabel.Parent = statusSection
|
|
279
|
+
|
|
280
|
+
-- Step-by-step status rows
|
|
281
|
+
local stepsFrame = Instance.new("Frame")
|
|
282
|
+
stepsFrame.Size = UDim2.new(1, 0, 0, 60)
|
|
283
|
+
stepsFrame.Position = UDim2.new(0, 0, 0, 68)
|
|
284
|
+
stepsFrame.BackgroundTransparency = 1
|
|
285
|
+
stepsFrame.Parent = statusSection
|
|
286
|
+
|
|
287
|
+
local stepsLayout = Instance.new("UIListLayout")
|
|
288
|
+
stepsLayout.Padding = UDim.new(0, 6)
|
|
289
|
+
stepsLayout.FillDirection = Enum.FillDirection.Vertical
|
|
290
|
+
stepsLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
291
|
+
stepsLayout.Parent = stepsFrame
|
|
292
|
+
|
|
293
|
+
local function createStepRow(text)
|
|
294
|
+
local row = Instance.new("Frame")
|
|
295
|
+
row.Size = UDim2.new(1, 0, 0, 16)
|
|
296
|
+
row.BackgroundTransparency = 1
|
|
297
|
+
|
|
298
|
+
local dot = Instance.new("Frame")
|
|
299
|
+
dot.Size = UDim2.new(0, 10, 0, 10)
|
|
300
|
+
dot.Position = UDim2.new(0, 0, 0, 3)
|
|
301
|
+
dot.BackgroundColor3 = Color3.fromRGB(156, 163, 175)
|
|
302
|
+
dot.BorderSizePixel = 0
|
|
303
|
+
dot.Parent = row
|
|
304
|
+
|
|
305
|
+
local dotCorner = Instance.new("UICorner")
|
|
306
|
+
dotCorner.CornerRadius = UDim.new(1, 0)
|
|
307
|
+
dotCorner.Parent = dot
|
|
308
|
+
|
|
309
|
+
local label = Instance.new("TextLabel")
|
|
310
|
+
label.Size = UDim2.new(1, -18, 1, 0)
|
|
311
|
+
label.Position = UDim2.new(0, 18, 0, 0)
|
|
312
|
+
label.BackgroundTransparency = 1
|
|
313
|
+
label.Text = text
|
|
314
|
+
label.TextColor3 = Color3.fromRGB(209, 213, 219)
|
|
315
|
+
label.TextScaled = false
|
|
316
|
+
label.TextSize = 11
|
|
317
|
+
label.Font = Enum.Font.Jura
|
|
318
|
+
label.TextXAlignment = Enum.TextXAlignment.Left
|
|
319
|
+
label.Parent = row
|
|
320
|
+
|
|
321
|
+
row.Parent = stepsFrame
|
|
322
|
+
return row, dot, label
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
local step1Row, step1Dot, step1Label = createStepRow("1. HTTP server reachable")
|
|
326
|
+
local step2Row, step2Dot, step2Label = createStepRow("2. MCP bridge connected")
|
|
327
|
+
local step3Row, step3Dot, step3Label = createStepRow("3. Ready for commands")
|
|
328
|
+
|
|
329
|
+
-- Troubleshooting tip for common stuck state
|
|
330
|
+
local troubleshootLabel = Instance.new("TextLabel")
|
|
331
|
+
troubleshootLabel.Size = UDim2.new(1, 0, 0, 40)
|
|
332
|
+
troubleshootLabel.Position = UDim2.new(0, 0, 0, 130)
|
|
333
|
+
troubleshootLabel.BackgroundTransparency = 1
|
|
334
|
+
troubleshootLabel.TextWrapped = true
|
|
335
|
+
troubleshootLabel.Visible = false
|
|
336
|
+
troubleshootLabel.Text = "HTTP is OK but MCP isn't responding. Close all node.exe in Task Manager and restart the server."
|
|
337
|
+
troubleshootLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
338
|
+
troubleshootLabel.TextScaled = false
|
|
339
|
+
troubleshootLabel.TextSize = 11
|
|
340
|
+
troubleshootLabel.Font = Enum.Font.Jura
|
|
341
|
+
troubleshootLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
342
|
+
troubleshootLabel.Parent = statusSection
|
|
343
|
+
|
|
344
|
+
local connectButton = Instance.new("TextButton")
|
|
345
|
+
connectButton.Size = UDim2.new(1, 0, 0, 48)
|
|
346
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(16, 185, 129)
|
|
347
|
+
connectButton.BorderSizePixel = 0
|
|
348
|
+
connectButton.Text = "Connect"
|
|
349
|
+
connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
350
|
+
connectButton.TextScaled = false
|
|
351
|
+
connectButton.TextSize = 16
|
|
352
|
+
connectButton.Font = Enum.Font.Jura
|
|
353
|
+
connectButton.LayoutOrder = 3
|
|
354
|
+
connectButton.Parent = contentFrame
|
|
355
|
+
|
|
356
|
+
local connectCorner = Instance.new("UICorner")
|
|
357
|
+
connectCorner.CornerRadius = UDim.new(0, 12)
|
|
358
|
+
connectCorner.Parent = connectButton
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
local TweenService = game:GetService("TweenService")
|
|
363
|
+
local buttonHover = false
|
|
364
|
+
|
|
365
|
+
connectButton.MouseEnter:Connect(function()
|
|
366
|
+
buttonHover = true
|
|
367
|
+
connectButton.BackgroundColor3 = not pluginState.isActive and Color3.fromRGB(5, 150, 105) or Color3.fromRGB(220, 38, 38)
|
|
368
|
+
end)
|
|
369
|
+
|
|
370
|
+
connectButton.MouseLeave:Connect(function()
|
|
371
|
+
buttonHover = false
|
|
372
|
+
connectButton.BackgroundColor3 = not pluginState.isActive and Color3.fromRGB(16, 185, 129) or Color3.fromRGB(239, 68, 68)
|
|
373
|
+
end)
|
|
374
|
+
|
|
375
|
+
local pulseAnimation = nil
|
|
376
|
+
|
|
377
|
+
local function createPulseAnimation()
|
|
378
|
+
if pulseAnimation then
|
|
379
|
+
pcall(function()
|
|
380
|
+
pulseAnimation:Cancel()
|
|
381
|
+
end)
|
|
382
|
+
pulseAnimation = nil
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
pcall(function()
|
|
386
|
+
pulseAnimation = TweenService:Create(statusPulse, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true), {
|
|
387
|
+
Size = UDim2.new(0, 24, 0, 24),
|
|
388
|
+
Position = UDim2.new(0, -4, 0, -4),
|
|
389
|
+
BackgroundTransparency = 1
|
|
390
|
+
})
|
|
391
|
+
end)
|
|
392
|
+
|
|
393
|
+
return pulseAnimation
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
local function stopPulseAnimation()
|
|
397
|
+
statusPulse.Size = UDim2.new(0, 16, 0, 16)
|
|
398
|
+
statusPulse.Position = UDim2.new(0, 0, 0, 0)
|
|
399
|
+
statusPulse.BackgroundTransparency = 0.7
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
local function startPulseAnimation()
|
|
403
|
+
statusPulse.Size = UDim2.new(0, 16, 0, 16)
|
|
404
|
+
statusPulse.Position = UDim2.new(0, 0, 0, 0)
|
|
405
|
+
statusPulse.BackgroundTransparency = 0.7
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
local function safeCall(func, ...)
|
|
409
|
+
local success, result = pcall(func, ...)
|
|
410
|
+
if success then
|
|
411
|
+
return result
|
|
412
|
+
else
|
|
413
|
+
warn("MCP Plugin Error: " .. tostring(result))
|
|
414
|
+
return nil
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
local function getInstancePath(instance)
|
|
419
|
+
if not instance or instance == game then
|
|
420
|
+
return "game"
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
local path = {}
|
|
424
|
+
local current = instance
|
|
425
|
+
|
|
426
|
+
while current and current ~= game do
|
|
427
|
+
table.insert(path, 1, current.Name)
|
|
428
|
+
current = current.Parent
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
return "game." .. table.concat(path, ".")
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
-- Helper to normalize line endings and split without inventing an extra trailing line
|
|
435
|
+
local function splitLines(source)
|
|
436
|
+
local normalized = (source or ""):gsub("\r\n", "\n"):gsub("\r", "\n")
|
|
437
|
+
local endsWithNewline = normalized:sub(-1) == "\n"
|
|
438
|
+
|
|
439
|
+
local lines = {}
|
|
440
|
+
local start = 1
|
|
441
|
+
|
|
442
|
+
while true do
|
|
443
|
+
local newlinePos = string.find(normalized, "\n", start, true)
|
|
444
|
+
if newlinePos then
|
|
445
|
+
table.insert(lines, string.sub(normalized, start, newlinePos - 1))
|
|
446
|
+
start = newlinePos + 1
|
|
447
|
+
else
|
|
448
|
+
local remainder = string.sub(normalized, start)
|
|
449
|
+
if remainder ~= "" or not endsWithNewline then
|
|
450
|
+
table.insert(lines, remainder)
|
|
451
|
+
end
|
|
452
|
+
break
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
if #lines == 0 then
|
|
457
|
+
table.insert(lines, "")
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
return lines, endsWithNewline
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
local function joinLines(lines, hadTrailingNewline)
|
|
464
|
+
local source = table.concat(lines, "\n")
|
|
465
|
+
if hadTrailingNewline and source:sub(-1) ~= "\n" then
|
|
466
|
+
source ..= "\n"
|
|
467
|
+
end
|
|
468
|
+
return source
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
-- Helper to convert property values from JSON to Roblox types
|
|
472
|
+
local function convertPropertyValue(instance, propertyName, propertyValue)
|
|
473
|
+
-- Handle nil
|
|
474
|
+
if propertyValue == nil then
|
|
475
|
+
return nil
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
-- Handle arrays (likely Vector3, Color3, UDim2)
|
|
479
|
+
if type(propertyValue) == "table" and #propertyValue > 0 then
|
|
480
|
+
-- Check if it's a Vector3-like property
|
|
481
|
+
if #propertyValue == 3 then
|
|
482
|
+
local prop = propertyName:lower()
|
|
483
|
+
if prop == "position" or prop == "size" or prop == "orientation" or prop == "velocity" or prop == "angularvelocity" then
|
|
484
|
+
return Vector3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
|
|
485
|
+
elseif prop == "color" or prop == "color3" then
|
|
486
|
+
return Color3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
|
|
487
|
+
else
|
|
488
|
+
-- Try to infer from current property type
|
|
489
|
+
local success, currentVal = pcall(function() return instance[propertyName] end)
|
|
490
|
+
if success then
|
|
491
|
+
if typeof(currentVal) == "Vector3" then
|
|
492
|
+
return Vector3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
|
|
493
|
+
elseif typeof(currentVal) == "Color3" then
|
|
494
|
+
return Color3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
elseif #propertyValue == 2 then
|
|
499
|
+
-- Possibly Vector2
|
|
500
|
+
local success, currentVal = pcall(function() return instance[propertyName] end)
|
|
501
|
+
if success and typeof(currentVal) == "Vector2" then
|
|
502
|
+
return Vector2.new(propertyValue[1] or 0, propertyValue[2] or 0)
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
-- Handle object with X, Y, Z keys (Vector3)
|
|
508
|
+
if type(propertyValue) == "table" and (propertyValue.X or propertyValue.Y or propertyValue.Z) then
|
|
509
|
+
return Vector3.new(propertyValue.X or 0, propertyValue.Y or 0, propertyValue.Z or 0)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
-- Handle object with R, G, B keys (Color3)
|
|
513
|
+
if type(propertyValue) == "table" and (propertyValue.R or propertyValue.G or propertyValue.B) then
|
|
514
|
+
return Color3.new(propertyValue.R or 0, propertyValue.G or 0, propertyValue.B or 0)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
-- Handle Enum values (strings like "Ball", "Cylinder", etc.)
|
|
518
|
+
if type(propertyValue) == "string" then
|
|
519
|
+
local success, currentVal = pcall(function() return instance[propertyName] end)
|
|
520
|
+
if success and typeof(currentVal) == "EnumItem" then
|
|
521
|
+
local enumType = tostring(currentVal.EnumType)
|
|
522
|
+
local enumSuccess, enumVal = pcall(function()
|
|
523
|
+
return Enum[enumType][propertyValue]
|
|
524
|
+
end)
|
|
525
|
+
if enumSuccess and enumVal then
|
|
526
|
+
return enumVal
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
-- Handle BrickColor
|
|
530
|
+
if propertyName == "BrickColor" then
|
|
531
|
+
return BrickColor.new(propertyValue)
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
-- Handle boolean strings
|
|
536
|
+
if type(propertyValue) == "string" then
|
|
537
|
+
if propertyValue == "true" then return true end
|
|
538
|
+
if propertyValue == "false" then return false end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
-- Return as-is for primitives (number, boolean, string)
|
|
542
|
+
return propertyValue
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
local processRequest
|
|
546
|
+
local sendResponse
|
|
547
|
+
local handlers = {}
|
|
548
|
+
|
|
549
|
+
local function pollForRequests()
|
|
550
|
+
if not pluginState.isActive then
|
|
551
|
+
return
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
local success, result = pcall(function()
|
|
555
|
+
return HttpService:RequestAsync({
|
|
556
|
+
Url = pluginState.serverUrl .. "/poll",
|
|
557
|
+
Method = "GET",
|
|
558
|
+
Headers = {
|
|
559
|
+
["Content-Type"] = "application/json",
|
|
560
|
+
},
|
|
561
|
+
})
|
|
562
|
+
end)
|
|
563
|
+
|
|
564
|
+
if success and (result.Success or result.StatusCode == 503) then
|
|
565
|
+
pluginState.consecutiveFailures = 0
|
|
566
|
+
pluginState.currentRetryDelay = 0.5
|
|
567
|
+
pluginState.lastSuccessfulConnection = tick()
|
|
568
|
+
|
|
569
|
+
local data = HttpService:JSONDecode(result.Body)
|
|
570
|
+
local mcpConnected = data.mcpConnected == true
|
|
571
|
+
-- Step indicators: HTTP request succeeded
|
|
572
|
+
pluginState.lastHttpOk = true
|
|
573
|
+
step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
574
|
+
step1Label.Text = "1. HTTP server reachable (OK)"
|
|
575
|
+
|
|
576
|
+
if mcpConnected and not statusLabel.Text:find("Connected") then
|
|
577
|
+
statusLabel.Text = "Connected"
|
|
578
|
+
statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
|
|
579
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
580
|
+
statusPulse.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
581
|
+
statusText.Text = "ONLINE"
|
|
582
|
+
detailStatusLabel.Text = "HTTP: OK MCP: OK"
|
|
583
|
+
detailStatusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
|
|
584
|
+
-- Steps 2/3 OK
|
|
585
|
+
step2Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
586
|
+
step2Label.Text = "2. MCP bridge connected (OK)"
|
|
587
|
+
step3Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
588
|
+
step3Label.Text = "3. Ready for commands (OK)"
|
|
589
|
+
pluginState.mcpWaitStartTime = nil
|
|
590
|
+
troubleshootLabel.Visible = false
|
|
591
|
+
stopPulseAnimation()
|
|
592
|
+
elseif not mcpConnected then
|
|
593
|
+
statusLabel.Text = "Waiting for MCP server"
|
|
594
|
+
statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
595
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
596
|
+
statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
597
|
+
statusText.Text = "WAITING"
|
|
598
|
+
detailStatusLabel.Text = "HTTP: OK MCP: ..."
|
|
599
|
+
detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
600
|
+
-- Step 2/3 pending
|
|
601
|
+
step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
602
|
+
step2Label.Text = "2. MCP bridge connected (waiting...)"
|
|
603
|
+
step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
604
|
+
step3Label.Text = "3. Ready for commands (waiting...)"
|
|
605
|
+
-- Track stuck state where HTTP is OK but MCP isn't
|
|
606
|
+
if not pluginState.mcpWaitStartTime then
|
|
607
|
+
pluginState.mcpWaitStartTime = tick()
|
|
608
|
+
end
|
|
609
|
+
local elapsed = tick() - (pluginState.mcpWaitStartTime or tick())
|
|
610
|
+
troubleshootLabel.Visible = elapsed > 8
|
|
611
|
+
startPulseAnimation()
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
if data.request and mcpConnected then
|
|
615
|
+
local response = processRequest(data.request)
|
|
616
|
+
sendResponse(data.requestId, response)
|
|
617
|
+
end
|
|
618
|
+
elseif pluginState.isActive then
|
|
619
|
+
pluginState.consecutiveFailures = pluginState.consecutiveFailures + 1
|
|
620
|
+
|
|
621
|
+
if pluginState.consecutiveFailures > 1 then
|
|
622
|
+
pluginState.currentRetryDelay =
|
|
623
|
+
math.min(pluginState.currentRetryDelay * pluginState.retryBackoffMultiplier, pluginState.maxRetryDelay)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
if pluginState.consecutiveFailures >= pluginState.maxFailuresBeforeError then
|
|
627
|
+
statusLabel.Text = "Server unavailable"
|
|
628
|
+
statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
629
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
630
|
+
statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
631
|
+
statusText.Text = "ERROR"
|
|
632
|
+
detailStatusLabel.Text = "HTTP: X MCP: X"
|
|
633
|
+
detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
634
|
+
-- Steps show error
|
|
635
|
+
step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
636
|
+
step1Label.Text = "1. HTTP server reachable (error)"
|
|
637
|
+
step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
638
|
+
step2Label.Text = "2. MCP bridge connected (error)"
|
|
639
|
+
step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
640
|
+
step3Label.Text = "3. Ready for commands (error)"
|
|
641
|
+
pluginState.mcpWaitStartTime = nil
|
|
642
|
+
troubleshootLabel.Visible = false
|
|
643
|
+
stopPulseAnimation()
|
|
644
|
+
elseif pluginState.consecutiveFailures > 5 then
|
|
645
|
+
local waitTime = math.ceil(pluginState.currentRetryDelay)
|
|
646
|
+
statusLabel.Text = "Retrying (" .. waitTime .. "s)"
|
|
647
|
+
statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
648
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
649
|
+
statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
650
|
+
statusText.Text = "RETRY"
|
|
651
|
+
detailStatusLabel.Text = "HTTP: ... MCP: ..."
|
|
652
|
+
detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
653
|
+
-- Steps show retrying
|
|
654
|
+
step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
655
|
+
step1Label.Text = "1. HTTP server reachable (retrying...)"
|
|
656
|
+
step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
657
|
+
step2Label.Text = "2. MCP bridge connected (retrying...)"
|
|
658
|
+
step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
659
|
+
step3Label.Text = "3. Ready for commands (retrying...)"
|
|
660
|
+
pluginState.mcpWaitStartTime = nil
|
|
661
|
+
troubleshootLabel.Visible = false
|
|
662
|
+
startPulseAnimation()
|
|
663
|
+
elseif pluginState.consecutiveFailures > 1 then
|
|
664
|
+
statusLabel.Text = "Connecting (attempt " .. pluginState.consecutiveFailures .. ")"
|
|
665
|
+
statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
666
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
667
|
+
statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
668
|
+
statusText.Text = "CONNECTING"
|
|
669
|
+
detailStatusLabel.Text = "HTTP: ... MCP: ..."
|
|
670
|
+
detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
671
|
+
-- Steps show connecting
|
|
672
|
+
step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
673
|
+
step1Label.Text = "1. HTTP server reachable (connecting...)"
|
|
674
|
+
step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
675
|
+
step2Label.Text = "2. MCP bridge connected (connecting...)"
|
|
676
|
+
step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
677
|
+
step3Label.Text = "3. Ready for commands (connecting...)"
|
|
678
|
+
pluginState.mcpWaitStartTime = nil
|
|
679
|
+
troubleshootLabel.Visible = false
|
|
680
|
+
startPulseAnimation()
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
sendResponse = function(requestId, responseData)
|
|
686
|
+
pcall(function()
|
|
687
|
+
HttpService:RequestAsync({
|
|
688
|
+
Url = pluginState.serverUrl .. "/response",
|
|
689
|
+
Method = "POST",
|
|
690
|
+
Headers = {
|
|
691
|
+
["Content-Type"] = "application/json",
|
|
692
|
+
},
|
|
693
|
+
Body = HttpService:JSONEncode({
|
|
694
|
+
requestId = requestId,
|
|
695
|
+
response = responseData,
|
|
696
|
+
}),
|
|
697
|
+
})
|
|
698
|
+
end)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
processRequest = function(request)
|
|
702
|
+
local endpoint = request.endpoint
|
|
703
|
+
local data = request.data or {}
|
|
704
|
+
|
|
705
|
+
if endpoint == "/api/file-tree" then
|
|
706
|
+
return handlers.getFileTree(data)
|
|
707
|
+
elseif endpoint == "/api/search-files" then
|
|
708
|
+
return handlers.searchFiles(data)
|
|
709
|
+
elseif endpoint == "/api/place-info" then
|
|
710
|
+
return handlers.getPlaceInfo(data)
|
|
711
|
+
elseif endpoint == "/api/services" then
|
|
712
|
+
return handlers.getServices(data)
|
|
713
|
+
elseif endpoint == "/api/search-objects" then
|
|
714
|
+
return handlers.searchObjects(data)
|
|
715
|
+
elseif endpoint == "/api/instance-properties" then
|
|
716
|
+
return handlers.getInstanceProperties(data)
|
|
717
|
+
elseif endpoint == "/api/instance-children" then
|
|
718
|
+
return handlers.getInstanceChildren(data)
|
|
719
|
+
elseif endpoint == "/api/search-by-property" then
|
|
720
|
+
return handlers.searchByProperty(data)
|
|
721
|
+
elseif endpoint == "/api/class-info" then
|
|
722
|
+
return handlers.getClassInfo(data)
|
|
723
|
+
elseif endpoint == "/api/project-structure" then
|
|
724
|
+
return handlers.getProjectStructure(data)
|
|
725
|
+
elseif endpoint == "/api/set-property" then
|
|
726
|
+
return handlers.setProperty(data)
|
|
727
|
+
elseif endpoint == "/api/mass-set-property" then
|
|
728
|
+
return handlers.massSetProperty(data)
|
|
729
|
+
elseif endpoint == "/api/mass-get-property" then
|
|
730
|
+
return handlers.massGetProperty(data)
|
|
731
|
+
elseif endpoint == "/api/create-object" then
|
|
732
|
+
return handlers.createObject(data)
|
|
733
|
+
elseif endpoint == "/api/mass-create-objects" then
|
|
734
|
+
return handlers.massCreateObjects(data)
|
|
735
|
+
elseif endpoint == "/api/mass-create-objects-with-properties" then
|
|
736
|
+
return handlers.massCreateObjectsWithProperties(data)
|
|
737
|
+
elseif endpoint == "/api/delete-object" then
|
|
738
|
+
return handlers.deleteObject(data)
|
|
739
|
+
elseif endpoint == "/api/smart-duplicate" then
|
|
740
|
+
return handlers.smartDuplicate(data)
|
|
741
|
+
elseif endpoint == "/api/mass-duplicate" then
|
|
742
|
+
return handlers.massDuplicate(data)
|
|
743
|
+
elseif endpoint == "/api/set-calculated-property" then
|
|
744
|
+
return handlers.setCalculatedProperty(data)
|
|
745
|
+
elseif endpoint == "/api/set-relative-property" then
|
|
746
|
+
return handlers.setRelativeProperty(data)
|
|
747
|
+
elseif endpoint == "/api/get-script-source" then
|
|
748
|
+
return handlers.getScriptSource(data)
|
|
749
|
+
elseif endpoint == "/api/set-script-source" then
|
|
750
|
+
return handlers.setScriptSource(data)
|
|
751
|
+
-- Partial script editing endpoints
|
|
752
|
+
elseif endpoint == "/api/edit-script-lines" then
|
|
753
|
+
return handlers.editScriptLines(data)
|
|
754
|
+
elseif endpoint == "/api/insert-script-lines" then
|
|
755
|
+
return handlers.insertScriptLines(data)
|
|
756
|
+
elseif endpoint == "/api/delete-script-lines" then
|
|
757
|
+
return handlers.deleteScriptLines(data)
|
|
758
|
+
-- Attribute endpoints
|
|
759
|
+
elseif endpoint == "/api/get-attribute" then
|
|
760
|
+
return handlers.getAttribute(data)
|
|
761
|
+
elseif endpoint == "/api/set-attribute" then
|
|
762
|
+
return handlers.setAttribute(data)
|
|
763
|
+
elseif endpoint == "/api/get-attributes" then
|
|
764
|
+
return handlers.getAttributes(data)
|
|
765
|
+
elseif endpoint == "/api/delete-attribute" then
|
|
766
|
+
return handlers.deleteAttribute(data)
|
|
767
|
+
-- Tag endpoints
|
|
768
|
+
elseif endpoint == "/api/get-tags" then
|
|
769
|
+
return handlers.getTags(data)
|
|
770
|
+
elseif endpoint == "/api/add-tag" then
|
|
771
|
+
return handlers.addTag(data)
|
|
772
|
+
elseif endpoint == "/api/remove-tag" then
|
|
773
|
+
return handlers.removeTag(data)
|
|
774
|
+
elseif endpoint == "/api/get-tagged" then
|
|
775
|
+
return handlers.getTagged(data)
|
|
776
|
+
-- Selection endpoints
|
|
777
|
+
elseif endpoint == "/api/get-selection" then
|
|
778
|
+
return handlers.getSelection(data)
|
|
779
|
+
else
|
|
780
|
+
return { error = "Unknown endpoint: " .. tostring(endpoint) }
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
local function getInstanceByPath(path)
|
|
785
|
+
if path == "game" or path == "" then
|
|
786
|
+
return game
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
path = path:gsub("^game%.", "")
|
|
790
|
+
|
|
791
|
+
local parts = {}
|
|
792
|
+
for part in path:gmatch("[^%.]+") do
|
|
793
|
+
table.insert(parts, part)
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
local current = game
|
|
797
|
+
for _, part in ipairs(parts) do
|
|
798
|
+
current = current:FindFirstChild(part)
|
|
799
|
+
if not current then
|
|
800
|
+
return nil
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
return current
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
handlers.getFileTree = function(requestData)
|
|
808
|
+
local path = requestData.path or ""
|
|
809
|
+
local startInstance = getInstanceByPath(path)
|
|
810
|
+
|
|
811
|
+
if not startInstance then
|
|
812
|
+
return { error = "Path not found: " .. path }
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
local function buildTree(instance, depth)
|
|
816
|
+
if depth > 10 then
|
|
817
|
+
return { name = instance.Name, className = instance.ClassName, children = {} }
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
local node = {
|
|
821
|
+
name = instance.Name,
|
|
822
|
+
className = instance.ClassName,
|
|
823
|
+
path = getInstancePath(instance),
|
|
824
|
+
children = {},
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if instance:IsA("LuaSourceContainer") then
|
|
828
|
+
node.hasSource = true
|
|
829
|
+
node.scriptType = instance.ClassName
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
for _, child in ipairs(instance:GetChildren()) do
|
|
833
|
+
table.insert(node.children, buildTree(child, depth + 1))
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
return node
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
return {
|
|
840
|
+
tree = buildTree(startInstance, 0),
|
|
841
|
+
timestamp = tick(),
|
|
842
|
+
}
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
handlers.searchFiles = function(requestData)
|
|
846
|
+
local query = requestData.query
|
|
847
|
+
local searchType = requestData.searchType or "name"
|
|
848
|
+
|
|
849
|
+
if not query then
|
|
850
|
+
return { error = "Query is required" }
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
local results = {}
|
|
854
|
+
|
|
855
|
+
local function searchRecursive(instance)
|
|
856
|
+
local match = false
|
|
857
|
+
|
|
858
|
+
if searchType == "name" then
|
|
859
|
+
match = instance.Name:lower():find(query:lower()) ~= nil
|
|
860
|
+
elseif searchType == "type" then
|
|
861
|
+
match = instance.ClassName:lower():find(query:lower()) ~= nil
|
|
862
|
+
elseif searchType == "content" and instance:IsA("LuaSourceContainer") then
|
|
863
|
+
match = instance.Source:lower():find(query:lower()) ~= nil
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
if match then
|
|
867
|
+
table.insert(results, {
|
|
868
|
+
name = instance.Name,
|
|
869
|
+
className = instance.ClassName,
|
|
870
|
+
path = getInstancePath(instance),
|
|
871
|
+
hasSource = instance:IsA("LuaSourceContainer"),
|
|
872
|
+
})
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
for _, child in ipairs(instance:GetChildren()) do
|
|
876
|
+
searchRecursive(child)
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
searchRecursive(game)
|
|
881
|
+
|
|
882
|
+
return {
|
|
883
|
+
results = results,
|
|
884
|
+
query = query,
|
|
885
|
+
searchType = searchType,
|
|
886
|
+
count = #results,
|
|
887
|
+
}
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
handlers.getPlaceInfo = function(requestData)
|
|
891
|
+
return {
|
|
892
|
+
placeName = game.Name,
|
|
893
|
+
placeId = game.PlaceId,
|
|
894
|
+
gameId = game.GameId,
|
|
895
|
+
jobId = game.JobId,
|
|
896
|
+
workspace = {
|
|
897
|
+
name = workspace.Name,
|
|
898
|
+
className = workspace.ClassName,
|
|
899
|
+
},
|
|
900
|
+
}
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
handlers.getServices = function(requestData)
|
|
904
|
+
local serviceName = requestData.serviceName
|
|
905
|
+
|
|
906
|
+
if serviceName then
|
|
907
|
+
local service = safeCall(game.GetService, game, serviceName)
|
|
908
|
+
if service then
|
|
909
|
+
return {
|
|
910
|
+
service = {
|
|
911
|
+
name = service.Name,
|
|
912
|
+
className = service.ClassName,
|
|
913
|
+
path = getInstancePath(service),
|
|
914
|
+
childCount = #service:GetChildren(),
|
|
915
|
+
},
|
|
916
|
+
}
|
|
917
|
+
else
|
|
918
|
+
return { error = "Service not found: " .. serviceName }
|
|
919
|
+
end
|
|
920
|
+
else
|
|
921
|
+
local services = {}
|
|
922
|
+
local commonServices = {
|
|
923
|
+
"Workspace",
|
|
924
|
+
"Players",
|
|
925
|
+
"StarterGui",
|
|
926
|
+
"StarterPack",
|
|
927
|
+
"StarterPlayer",
|
|
928
|
+
"ReplicatedStorage",
|
|
929
|
+
"ServerStorage",
|
|
930
|
+
"ServerScriptService",
|
|
931
|
+
"HttpService",
|
|
932
|
+
"TeleportService",
|
|
933
|
+
"DataStoreService",
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
for _, serviceName in ipairs(commonServices) do
|
|
937
|
+
local service = safeCall(game.GetService, game, serviceName)
|
|
938
|
+
if service then
|
|
939
|
+
table.insert(services, {
|
|
940
|
+
name = service.Name,
|
|
941
|
+
className = service.ClassName,
|
|
942
|
+
path = getInstancePath(service),
|
|
943
|
+
childCount = #service:GetChildren(),
|
|
944
|
+
})
|
|
945
|
+
end
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
return { services = services }
|
|
949
|
+
end
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
handlers.searchObjects = function(requestData)
|
|
953
|
+
local query = requestData.query
|
|
954
|
+
local searchType = requestData.searchType or "name"
|
|
955
|
+
local propertyName = requestData.propertyName
|
|
956
|
+
|
|
957
|
+
if not query then
|
|
958
|
+
return { error = "Query is required" }
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
local results = {}
|
|
962
|
+
|
|
963
|
+
local function searchRecursive(instance)
|
|
964
|
+
local match = false
|
|
965
|
+
|
|
966
|
+
if searchType == "name" then
|
|
967
|
+
match = instance.Name:lower():find(query:lower()) ~= nil
|
|
968
|
+
elseif searchType == "class" then
|
|
969
|
+
match = instance.ClassName:lower():find(query:lower()) ~= nil
|
|
970
|
+
elseif searchType == "property" and propertyName then
|
|
971
|
+
local success, value = pcall(function()
|
|
972
|
+
return tostring(instance[propertyName])
|
|
973
|
+
end)
|
|
974
|
+
if success then
|
|
975
|
+
match = value:lower():find(query:lower()) ~= nil
|
|
976
|
+
end
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
if match then
|
|
980
|
+
table.insert(results, {
|
|
981
|
+
name = instance.Name,
|
|
982
|
+
className = instance.ClassName,
|
|
983
|
+
path = getInstancePath(instance),
|
|
984
|
+
})
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
for _, child in ipairs(instance:GetChildren()) do
|
|
988
|
+
searchRecursive(child)
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
searchRecursive(game)
|
|
993
|
+
|
|
994
|
+
return {
|
|
995
|
+
results = results,
|
|
996
|
+
query = query,
|
|
997
|
+
searchType = searchType,
|
|
998
|
+
count = #results,
|
|
999
|
+
}
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
handlers.getInstanceProperties = function(requestData)
|
|
1003
|
+
local instancePath = requestData.instancePath
|
|
1004
|
+
if not instancePath then
|
|
1005
|
+
return { error = "Instance path is required" }
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
local instance = getInstanceByPath(instancePath)
|
|
1009
|
+
if not instance then
|
|
1010
|
+
return { error = "Instance not found: " .. instancePath }
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
local properties = {}
|
|
1014
|
+
local success, result = pcall(function()
|
|
1015
|
+
local classInfo = {}
|
|
1016
|
+
|
|
1017
|
+
local basicProps = { "Name", "ClassName", "Parent" }
|
|
1018
|
+
for _, prop in ipairs(basicProps) do
|
|
1019
|
+
local propSuccess, propValue = pcall(function()
|
|
1020
|
+
local val = instance[prop]
|
|
1021
|
+
if prop == "Parent" and val then
|
|
1022
|
+
return getInstancePath(val)
|
|
1023
|
+
elseif val == nil then
|
|
1024
|
+
return "nil"
|
|
1025
|
+
else
|
|
1026
|
+
return tostring(val)
|
|
1027
|
+
end
|
|
1028
|
+
end)
|
|
1029
|
+
if propSuccess then
|
|
1030
|
+
properties[prop] = propValue
|
|
1031
|
+
end
|
|
1032
|
+
end
|
|
1033
|
+
|
|
1034
|
+
local commonProps = {
|
|
1035
|
+
"Size",
|
|
1036
|
+
"Position",
|
|
1037
|
+
"Rotation",
|
|
1038
|
+
"CFrame",
|
|
1039
|
+
"Anchored",
|
|
1040
|
+
"CanCollide",
|
|
1041
|
+
"Transparency",
|
|
1042
|
+
"BrickColor",
|
|
1043
|
+
"Material",
|
|
1044
|
+
"Color",
|
|
1045
|
+
"Text",
|
|
1046
|
+
"TextColor3",
|
|
1047
|
+
"BackgroundColor3",
|
|
1048
|
+
"Image",
|
|
1049
|
+
"ImageColor3",
|
|
1050
|
+
"Visible",
|
|
1051
|
+
"Active",
|
|
1052
|
+
"ZIndex",
|
|
1053
|
+
"BorderSizePixel",
|
|
1054
|
+
"BackgroundTransparency",
|
|
1055
|
+
"ImageTransparency",
|
|
1056
|
+
"TextTransparency",
|
|
1057
|
+
"Value",
|
|
1058
|
+
"Enabled",
|
|
1059
|
+
"Brightness",
|
|
1060
|
+
"Range",
|
|
1061
|
+
"Shadows",
|
|
1062
|
+
"Face",
|
|
1063
|
+
"SurfaceType",
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
for _, prop in ipairs(commonProps) do
|
|
1067
|
+
local propSuccess, propValue = pcall(function()
|
|
1068
|
+
return tostring(instance[prop])
|
|
1069
|
+
end)
|
|
1070
|
+
if propSuccess then
|
|
1071
|
+
properties[prop] = propValue
|
|
1072
|
+
end
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
if instance:IsA("LuaSourceContainer") then
|
|
1076
|
+
properties.Source = instance.Source
|
|
1077
|
+
if instance:IsA("BaseScript") then
|
|
1078
|
+
properties.Enabled = tostring(instance.Enabled)
|
|
1079
|
+
end
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
-- Only Parts have a Shape property; MeshParts do not.
|
|
1083
|
+
if instance:IsA("Part") then
|
|
1084
|
+
properties.Shape = tostring(instance.Shape)
|
|
1085
|
+
end
|
|
1086
|
+
|
|
1087
|
+
-- TopSurface and BottomSurface exist on all BaseParts
|
|
1088
|
+
if instance:IsA("BasePart") then
|
|
1089
|
+
properties.TopSurface = tostring(instance.TopSurface)
|
|
1090
|
+
properties.BottomSurface = tostring(instance.BottomSurface)
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
properties.ChildCount = tostring(#instance:GetChildren())
|
|
1094
|
+
|
|
1095
|
+
return properties
|
|
1096
|
+
end)
|
|
1097
|
+
|
|
1098
|
+
if success then
|
|
1099
|
+
return {
|
|
1100
|
+
instancePath = instancePath,
|
|
1101
|
+
className = instance.ClassName,
|
|
1102
|
+
properties = properties,
|
|
1103
|
+
}
|
|
1104
|
+
else
|
|
1105
|
+
return { error = "Failed to get properties: " .. tostring(result) }
|
|
1106
|
+
end
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
handlers.getInstanceChildren = function(requestData)
|
|
1110
|
+
local instancePath = requestData.instancePath
|
|
1111
|
+
if not instancePath then
|
|
1112
|
+
return { error = "Instance path is required" }
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
local instance = getInstanceByPath(instancePath)
|
|
1116
|
+
if not instance then
|
|
1117
|
+
return { error = "Instance not found: " .. instancePath }
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
local children = {}
|
|
1121
|
+
for _, child in ipairs(instance:GetChildren()) do
|
|
1122
|
+
table.insert(children, {
|
|
1123
|
+
name = child.Name,
|
|
1124
|
+
className = child.ClassName,
|
|
1125
|
+
path = getInstancePath(child),
|
|
1126
|
+
hasChildren = #child:GetChildren() > 0,
|
|
1127
|
+
hasSource = child:IsA("LuaSourceContainer"),
|
|
1128
|
+
})
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
return {
|
|
1132
|
+
instancePath = instancePath,
|
|
1133
|
+
children = children,
|
|
1134
|
+
count = #children,
|
|
1135
|
+
}
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
handlers.searchByProperty = function(requestData)
|
|
1139
|
+
local propertyName = requestData.propertyName
|
|
1140
|
+
local propertyValue = requestData.propertyValue
|
|
1141
|
+
|
|
1142
|
+
if not propertyName or not propertyValue then
|
|
1143
|
+
return { error = "Property name and value are required" }
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
local results = {}
|
|
1147
|
+
|
|
1148
|
+
local function searchRecursive(instance)
|
|
1149
|
+
local success, value = pcall(function()
|
|
1150
|
+
return tostring(instance[propertyName])
|
|
1151
|
+
end)
|
|
1152
|
+
|
|
1153
|
+
if success and value:lower():find(propertyValue:lower()) then
|
|
1154
|
+
table.insert(results, {
|
|
1155
|
+
name = instance.Name,
|
|
1156
|
+
className = instance.ClassName,
|
|
1157
|
+
path = getInstancePath(instance),
|
|
1158
|
+
propertyValue = value,
|
|
1159
|
+
})
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
for _, child in ipairs(instance:GetChildren()) do
|
|
1163
|
+
searchRecursive(child)
|
|
1164
|
+
end
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
searchRecursive(game)
|
|
1168
|
+
|
|
1169
|
+
return {
|
|
1170
|
+
propertyName = propertyName,
|
|
1171
|
+
propertyValue = propertyValue,
|
|
1172
|
+
results = results,
|
|
1173
|
+
count = #results,
|
|
1174
|
+
}
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
handlers.getClassInfo = function(requestData)
|
|
1178
|
+
local className = requestData.className
|
|
1179
|
+
if not className then
|
|
1180
|
+
return { error = "Class name is required" }
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
local success, tempInstance = pcall(function()
|
|
1184
|
+
return Instance.new(className)
|
|
1185
|
+
end)
|
|
1186
|
+
|
|
1187
|
+
if not success then
|
|
1188
|
+
return { error = "Invalid class name: " .. className }
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
local classInfo = {
|
|
1192
|
+
className = className,
|
|
1193
|
+
properties = {},
|
|
1194
|
+
methods = {},
|
|
1195
|
+
events = {},
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
local commonProps = {
|
|
1199
|
+
"Name",
|
|
1200
|
+
"ClassName",
|
|
1201
|
+
"Parent",
|
|
1202
|
+
"Size",
|
|
1203
|
+
"Position",
|
|
1204
|
+
"Rotation",
|
|
1205
|
+
"CFrame",
|
|
1206
|
+
"Anchored",
|
|
1207
|
+
"CanCollide",
|
|
1208
|
+
"Transparency",
|
|
1209
|
+
"BrickColor",
|
|
1210
|
+
"Material",
|
|
1211
|
+
"Color",
|
|
1212
|
+
"Text",
|
|
1213
|
+
"TextColor3",
|
|
1214
|
+
"BackgroundColor3",
|
|
1215
|
+
"Image",
|
|
1216
|
+
"ImageColor3",
|
|
1217
|
+
"Visible",
|
|
1218
|
+
"Active",
|
|
1219
|
+
"ZIndex",
|
|
1220
|
+
"BorderSizePixel",
|
|
1221
|
+
"BackgroundTransparency",
|
|
1222
|
+
"ImageTransparency",
|
|
1223
|
+
"TextTransparency",
|
|
1224
|
+
"Value",
|
|
1225
|
+
"Enabled",
|
|
1226
|
+
"Brightness",
|
|
1227
|
+
"Range",
|
|
1228
|
+
"Shadows",
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
for _, prop in ipairs(commonProps) do
|
|
1232
|
+
local propSuccess, _ = pcall(function()
|
|
1233
|
+
return tempInstance[prop]
|
|
1234
|
+
end)
|
|
1235
|
+
if propSuccess then
|
|
1236
|
+
table.insert(classInfo.properties, prop)
|
|
1237
|
+
end
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
local commonMethods = {
|
|
1241
|
+
"Destroy",
|
|
1242
|
+
"Clone",
|
|
1243
|
+
"FindFirstChild",
|
|
1244
|
+
"FindFirstChildOfClass",
|
|
1245
|
+
"GetChildren",
|
|
1246
|
+
"IsA",
|
|
1247
|
+
"IsAncestorOf",
|
|
1248
|
+
"IsDescendantOf",
|
|
1249
|
+
"WaitForChild",
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
for _, method in ipairs(commonMethods) do
|
|
1253
|
+
local methodSuccess, _ = pcall(function()
|
|
1254
|
+
return tempInstance[method]
|
|
1255
|
+
end)
|
|
1256
|
+
if methodSuccess then
|
|
1257
|
+
table.insert(classInfo.methods, method)
|
|
1258
|
+
end
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
tempInstance:Destroy()
|
|
1262
|
+
|
|
1263
|
+
return classInfo
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
handlers.getProjectStructure = function(requestData)
|
|
1267
|
+
local startPath = requestData.path or ""
|
|
1268
|
+
local maxDepth = requestData.maxDepth or 3
|
|
1269
|
+
local showScriptsOnly = requestData.scriptsOnly or false
|
|
1270
|
+
|
|
1271
|
+
local startInstance
|
|
1272
|
+
if startPath == "" or startPath == "game" then
|
|
1273
|
+
local services = {}
|
|
1274
|
+
local mainServices = {
|
|
1275
|
+
"Workspace",
|
|
1276
|
+
"ServerScriptService",
|
|
1277
|
+
"ServerStorage",
|
|
1278
|
+
"ReplicatedStorage",
|
|
1279
|
+
"StarterGui",
|
|
1280
|
+
"StarterPack",
|
|
1281
|
+
"StarterPlayer",
|
|
1282
|
+
"Players",
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
for _, serviceName in ipairs(mainServices) do
|
|
1286
|
+
local service = safeCall(game.GetService, game, serviceName)
|
|
1287
|
+
if service then
|
|
1288
|
+
local serviceInfo = {
|
|
1289
|
+
name = service.Name,
|
|
1290
|
+
className = service.ClassName,
|
|
1291
|
+
path = getInstancePath(service),
|
|
1292
|
+
childCount = #service:GetChildren(),
|
|
1293
|
+
hasChildren = #service:GetChildren() > 0,
|
|
1294
|
+
}
|
|
1295
|
+
table.insert(services, serviceInfo)
|
|
1296
|
+
end
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
return {
|
|
1300
|
+
type = "service_overview",
|
|
1301
|
+
services = services,
|
|
1302
|
+
timestamp = tick(),
|
|
1303
|
+
note = "Use path parameter to explore specific locations (e.g., 'game.ServerScriptService')",
|
|
1304
|
+
}
|
|
1305
|
+
else
|
|
1306
|
+
startInstance = getInstanceByPath(startPath)
|
|
1307
|
+
if not startInstance then
|
|
1308
|
+
return { error = "Path not found: " .. startPath }
|
|
1309
|
+
end
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
local function getStructure(instance, depth, currentPath)
|
|
1313
|
+
if depth > maxDepth then
|
|
1314
|
+
return {
|
|
1315
|
+
name = instance.Name,
|
|
1316
|
+
className = instance.ClassName,
|
|
1317
|
+
path = getInstancePath(instance),
|
|
1318
|
+
childCount = #instance:GetChildren(),
|
|
1319
|
+
hasMore = true,
|
|
1320
|
+
note = "Max depth reached - use this path to explore further",
|
|
1321
|
+
}
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
local node = {
|
|
1325
|
+
name = instance.Name,
|
|
1326
|
+
className = instance.ClassName,
|
|
1327
|
+
path = getInstancePath(instance),
|
|
1328
|
+
children = {},
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if instance:IsA("LuaSourceContainer") then
|
|
1332
|
+
node.hasSource = true
|
|
1333
|
+
node.scriptType = instance.ClassName
|
|
1334
|
+
if instance:IsA("BaseScript") then
|
|
1335
|
+
node.enabled = instance.Enabled
|
|
1336
|
+
end
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
if instance:IsA("GuiObject") then
|
|
1340
|
+
node.visible = instance.Visible
|
|
1341
|
+
if instance:IsA("Frame") or instance:IsA("ScreenGui") then
|
|
1342
|
+
node.guiType = "container"
|
|
1343
|
+
elseif instance:IsA("TextLabel") or instance:IsA("TextButton") then
|
|
1344
|
+
node.guiType = "text"
|
|
1345
|
+
if instance.Text and instance.Text ~= "" then
|
|
1346
|
+
node.text = instance.Text
|
|
1347
|
+
end
|
|
1348
|
+
elseif instance:IsA("ImageLabel") or instance:IsA("ImageButton") then
|
|
1349
|
+
node.guiType = "image"
|
|
1350
|
+
end
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
local children = instance:GetChildren()
|
|
1354
|
+
if showScriptsOnly then
|
|
1355
|
+
local scriptChildren = {}
|
|
1356
|
+
for _, child in ipairs(children) do
|
|
1357
|
+
if child:IsA("BaseScript") or child:IsA("Folder") or child:IsA("ModuleScript") then
|
|
1358
|
+
table.insert(scriptChildren, child)
|
|
1359
|
+
end
|
|
1360
|
+
end
|
|
1361
|
+
children = scriptChildren
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
local childCount = #children
|
|
1365
|
+
if childCount > 20 and depth < maxDepth then
|
|
1366
|
+
local classGroups = {}
|
|
1367
|
+
for _, child in ipairs(children) do
|
|
1368
|
+
local className = child.ClassName
|
|
1369
|
+
if not classGroups[className] then
|
|
1370
|
+
classGroups[className] = {}
|
|
1371
|
+
end
|
|
1372
|
+
table.insert(classGroups[className], child)
|
|
1373
|
+
end
|
|
1374
|
+
|
|
1375
|
+
node.childSummary = {}
|
|
1376
|
+
for className, classChildren in pairs(classGroups) do
|
|
1377
|
+
table.insert(node.childSummary, {
|
|
1378
|
+
className = className,
|
|
1379
|
+
count = #classChildren,
|
|
1380
|
+
examples = {
|
|
1381
|
+
classChildren[1] and classChildren[1].Name,
|
|
1382
|
+
classChildren[2] and classChildren[2].Name,
|
|
1383
|
+
},
|
|
1384
|
+
})
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
for className, classChildren in pairs(classGroups) do
|
|
1388
|
+
for i = 1, math.min(3, #classChildren) do
|
|
1389
|
+
table.insert(node.children, getStructure(classChildren[i], depth + 1, currentPath))
|
|
1390
|
+
end
|
|
1391
|
+
if #classChildren > 3 then
|
|
1392
|
+
table.insert(node.children, {
|
|
1393
|
+
name = "... " .. (#classChildren - 3) .. " more " .. className .. " objects",
|
|
1394
|
+
className = "MoreIndicator",
|
|
1395
|
+
path = getInstancePath(instance) .. " [" .. className .. " children]",
|
|
1396
|
+
note = "Use specific path to explore these objects",
|
|
1397
|
+
})
|
|
1398
|
+
end
|
|
1399
|
+
end
|
|
1400
|
+
else
|
|
1401
|
+
for _, child in ipairs(children) do
|
|
1402
|
+
table.insert(node.children, getStructure(child, depth + 1, currentPath))
|
|
1403
|
+
end
|
|
1404
|
+
end
|
|
1405
|
+
|
|
1406
|
+
return node
|
|
1407
|
+
end
|
|
1408
|
+
|
|
1409
|
+
local result = getStructure(startInstance, 0, startPath)
|
|
1410
|
+
result.requestedPath = startPath
|
|
1411
|
+
result.maxDepth = maxDepth
|
|
1412
|
+
result.scriptsOnly = showScriptsOnly
|
|
1413
|
+
result.timestamp = tick()
|
|
1414
|
+
|
|
1415
|
+
return result
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
handlers.setProperty = function(requestData)
|
|
1419
|
+
local instancePath = requestData.instancePath
|
|
1420
|
+
local propertyName = requestData.propertyName
|
|
1421
|
+
local propertyValue = requestData.propertyValue
|
|
1422
|
+
|
|
1423
|
+
if not instancePath or not propertyName then
|
|
1424
|
+
return { error = "Instance path and property name are required" }
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
local instance = getInstanceByPath(instancePath)
|
|
1428
|
+
if not instance then
|
|
1429
|
+
return { error = "Instance not found: " .. instancePath }
|
|
1430
|
+
end
|
|
1431
|
+
|
|
1432
|
+
local success, result = pcall(function()
|
|
1433
|
+
-- Handle instance reference properties (Parent, PrimaryPart, etc.)
|
|
1434
|
+
if propertyName == "Parent" or propertyName == "PrimaryPart" then
|
|
1435
|
+
if type(propertyValue) == "string" then
|
|
1436
|
+
local refInstance = getInstanceByPath(propertyValue)
|
|
1437
|
+
if refInstance then
|
|
1438
|
+
instance[propertyName] = refInstance
|
|
1439
|
+
else
|
|
1440
|
+
return { error = propertyName .. " instance not found: " .. propertyValue }
|
|
1441
|
+
end
|
|
1442
|
+
end
|
|
1443
|
+
elseif propertyName == "Name" then
|
|
1444
|
+
instance.Name = tostring(propertyValue)
|
|
1445
|
+
elseif propertyName == "Source" and instance:IsA("LuaSourceContainer") then
|
|
1446
|
+
instance.Source = tostring(propertyValue)
|
|
1447
|
+
else
|
|
1448
|
+
-- Use the generic converter for all other properties
|
|
1449
|
+
local convertedValue = convertPropertyValue(instance, propertyName, propertyValue)
|
|
1450
|
+
if convertedValue ~= nil then
|
|
1451
|
+
instance[propertyName] = convertedValue
|
|
1452
|
+
else
|
|
1453
|
+
instance[propertyName] = propertyValue
|
|
1454
|
+
end
|
|
1455
|
+
end
|
|
1456
|
+
|
|
1457
|
+
ChangeHistoryService:SetWaypoint("Set " .. propertyName .. " property")
|
|
1458
|
+
return true
|
|
1459
|
+
end)
|
|
1460
|
+
|
|
1461
|
+
if success and result ~= false then
|
|
1462
|
+
return {
|
|
1463
|
+
success = true,
|
|
1464
|
+
instancePath = instancePath,
|
|
1465
|
+
propertyName = propertyName,
|
|
1466
|
+
propertyValue = propertyValue,
|
|
1467
|
+
message = "Property set successfully",
|
|
1468
|
+
}
|
|
1469
|
+
else
|
|
1470
|
+
return {
|
|
1471
|
+
error = "Failed to set property: " .. tostring(result),
|
|
1472
|
+
instancePath = instancePath,
|
|
1473
|
+
propertyName = propertyName,
|
|
1474
|
+
}
|
|
1475
|
+
end
|
|
1476
|
+
end
|
|
1477
|
+
|
|
1478
|
+
handlers.createObject = function(requestData)
|
|
1479
|
+
local className = requestData.className
|
|
1480
|
+
local parentPath = requestData.parent
|
|
1481
|
+
local name = requestData.name
|
|
1482
|
+
local properties = requestData.properties or {}
|
|
1483
|
+
|
|
1484
|
+
if not className or not parentPath then
|
|
1485
|
+
return { error = "Class name and parent are required" }
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
local parentInstance = getInstanceByPath(parentPath)
|
|
1489
|
+
if not parentInstance then
|
|
1490
|
+
return { error = "Parent instance not found: " .. parentPath }
|
|
1491
|
+
end
|
|
1492
|
+
|
|
1493
|
+
local success, newInstance = pcall(function()
|
|
1494
|
+
local instance = Instance.new(className)
|
|
1495
|
+
|
|
1496
|
+
if name then
|
|
1497
|
+
instance.Name = name
|
|
1498
|
+
end
|
|
1499
|
+
|
|
1500
|
+
for propertyName, propertyValue in pairs(properties) do
|
|
1501
|
+
pcall(function()
|
|
1502
|
+
instance[propertyName] = propertyValue
|
|
1503
|
+
end)
|
|
1504
|
+
end
|
|
1505
|
+
|
|
1506
|
+
instance.Parent = parentInstance
|
|
1507
|
+
ChangeHistoryService:SetWaypoint("Create " .. className)
|
|
1508
|
+
return instance
|
|
1509
|
+
end)
|
|
1510
|
+
|
|
1511
|
+
if success and newInstance then
|
|
1512
|
+
return {
|
|
1513
|
+
success = true,
|
|
1514
|
+
className = className,
|
|
1515
|
+
parent = parentPath,
|
|
1516
|
+
instancePath = getInstancePath(newInstance),
|
|
1517
|
+
name = newInstance.Name,
|
|
1518
|
+
message = "Object created successfully",
|
|
1519
|
+
}
|
|
1520
|
+
else
|
|
1521
|
+
return {
|
|
1522
|
+
error = "Failed to create object: " .. tostring(newInstance),
|
|
1523
|
+
className = className,
|
|
1524
|
+
parent = parentPath,
|
|
1525
|
+
}
|
|
1526
|
+
end
|
|
1527
|
+
end
|
|
1528
|
+
|
|
1529
|
+
handlers.deleteObject = function(requestData)
|
|
1530
|
+
local instancePath = requestData.instancePath
|
|
1531
|
+
|
|
1532
|
+
if not instancePath then
|
|
1533
|
+
return { error = "Instance path is required" }
|
|
1534
|
+
end
|
|
1535
|
+
|
|
1536
|
+
local instance = getInstanceByPath(instancePath)
|
|
1537
|
+
if not instance then
|
|
1538
|
+
return { error = "Instance not found: " .. instancePath }
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1541
|
+
if instance == game then
|
|
1542
|
+
return { error = "Cannot delete the game instance" }
|
|
1543
|
+
end
|
|
1544
|
+
|
|
1545
|
+
local success, result = pcall(function()
|
|
1546
|
+
local name = instance.Name
|
|
1547
|
+
local className = instance.ClassName
|
|
1548
|
+
instance:Destroy()
|
|
1549
|
+
ChangeHistoryService:SetWaypoint("Delete " .. className .. " (" .. name .. ")")
|
|
1550
|
+
return true
|
|
1551
|
+
end)
|
|
1552
|
+
|
|
1553
|
+
if success then
|
|
1554
|
+
return {
|
|
1555
|
+
success = true,
|
|
1556
|
+
instancePath = instancePath,
|
|
1557
|
+
message = "Object deleted successfully",
|
|
1558
|
+
}
|
|
1559
|
+
else
|
|
1560
|
+
return {
|
|
1561
|
+
error = "Failed to delete object: " .. tostring(result),
|
|
1562
|
+
instancePath = instancePath,
|
|
1563
|
+
}
|
|
1564
|
+
end
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
handlers.massSetProperty = function(requestData)
|
|
1568
|
+
local paths = requestData.paths
|
|
1569
|
+
local propertyName = requestData.propertyName
|
|
1570
|
+
local propertyValue = requestData.propertyValue
|
|
1571
|
+
|
|
1572
|
+
if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName then
|
|
1573
|
+
return { error = "Paths array and property name are required" }
|
|
1574
|
+
end
|
|
1575
|
+
|
|
1576
|
+
local results = {}
|
|
1577
|
+
local successCount = 0
|
|
1578
|
+
local failureCount = 0
|
|
1579
|
+
|
|
1580
|
+
for _, path in ipairs(paths) do
|
|
1581
|
+
local instance = getInstanceByPath(path)
|
|
1582
|
+
if instance then
|
|
1583
|
+
local success, err = pcall(function()
|
|
1584
|
+
instance[propertyName] = propertyValue
|
|
1585
|
+
end)
|
|
1586
|
+
|
|
1587
|
+
if success then
|
|
1588
|
+
successCount = successCount + 1
|
|
1589
|
+
table.insert(results, {
|
|
1590
|
+
path = path,
|
|
1591
|
+
success = true,
|
|
1592
|
+
propertyName = propertyName,
|
|
1593
|
+
propertyValue = propertyValue
|
|
1594
|
+
})
|
|
1595
|
+
else
|
|
1596
|
+
failureCount = failureCount + 1
|
|
1597
|
+
table.insert(results, {
|
|
1598
|
+
path = path,
|
|
1599
|
+
success = false,
|
|
1600
|
+
error = tostring(err)
|
|
1601
|
+
})
|
|
1602
|
+
end
|
|
1603
|
+
else
|
|
1604
|
+
failureCount = failureCount + 1
|
|
1605
|
+
table.insert(results, {
|
|
1606
|
+
path = path,
|
|
1607
|
+
success = false,
|
|
1608
|
+
error = "Instance not found"
|
|
1609
|
+
})
|
|
1610
|
+
end
|
|
1611
|
+
end
|
|
1612
|
+
|
|
1613
|
+
if successCount > 0 then
|
|
1614
|
+
ChangeHistoryService:SetWaypoint("Mass set " .. propertyName .. " property")
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
return {
|
|
1618
|
+
results = results,
|
|
1619
|
+
summary = {
|
|
1620
|
+
total = #paths,
|
|
1621
|
+
succeeded = successCount,
|
|
1622
|
+
failed = failureCount
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
handlers.massGetProperty = function(requestData)
|
|
1628
|
+
local paths = requestData.paths
|
|
1629
|
+
local propertyName = requestData.propertyName
|
|
1630
|
+
|
|
1631
|
+
if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName then
|
|
1632
|
+
return { error = "Paths array and property name are required" }
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1635
|
+
local results = {}
|
|
1636
|
+
|
|
1637
|
+
for _, path in ipairs(paths) do
|
|
1638
|
+
local instance = getInstanceByPath(path)
|
|
1639
|
+
if instance then
|
|
1640
|
+
local success, value = pcall(function()
|
|
1641
|
+
return instance[propertyName]
|
|
1642
|
+
end)
|
|
1643
|
+
|
|
1644
|
+
if success then
|
|
1645
|
+
table.insert(results, {
|
|
1646
|
+
path = path,
|
|
1647
|
+
success = true,
|
|
1648
|
+
propertyName = propertyName,
|
|
1649
|
+
propertyValue = value
|
|
1650
|
+
})
|
|
1651
|
+
else
|
|
1652
|
+
table.insert(results, {
|
|
1653
|
+
path = path,
|
|
1654
|
+
success = false,
|
|
1655
|
+
error = tostring(value)
|
|
1656
|
+
})
|
|
1657
|
+
end
|
|
1658
|
+
else
|
|
1659
|
+
table.insert(results, {
|
|
1660
|
+
path = path,
|
|
1661
|
+
success = false,
|
|
1662
|
+
error = "Instance not found"
|
|
1663
|
+
})
|
|
1664
|
+
end
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1667
|
+
return {
|
|
1668
|
+
results = results,
|
|
1669
|
+
propertyName = propertyName
|
|
1670
|
+
}
|
|
1671
|
+
end
|
|
1672
|
+
|
|
1673
|
+
handlers.massCreateObjects = function(requestData)
|
|
1674
|
+
local objects = requestData.objects
|
|
1675
|
+
|
|
1676
|
+
if not objects or type(objects) ~= "table" or #objects == 0 then
|
|
1677
|
+
return { error = "Objects array is required" }
|
|
1678
|
+
end
|
|
1679
|
+
|
|
1680
|
+
local results = {}
|
|
1681
|
+
local successCount = 0
|
|
1682
|
+
local failureCount = 0
|
|
1683
|
+
|
|
1684
|
+
for _, objData in ipairs(objects) do
|
|
1685
|
+
local className = objData.className
|
|
1686
|
+
local parentPath = objData.parent
|
|
1687
|
+
local name = objData.name
|
|
1688
|
+
|
|
1689
|
+
if className and parentPath then
|
|
1690
|
+
local parentInstance = getInstanceByPath(parentPath)
|
|
1691
|
+
if parentInstance then
|
|
1692
|
+
local success, newInstance = pcall(function()
|
|
1693
|
+
local instance = Instance.new(className)
|
|
1694
|
+
if name then
|
|
1695
|
+
instance.Name = name
|
|
1696
|
+
end
|
|
1697
|
+
instance.Parent = parentInstance
|
|
1698
|
+
return instance
|
|
1699
|
+
end)
|
|
1700
|
+
|
|
1701
|
+
if success and newInstance then
|
|
1702
|
+
successCount = successCount + 1
|
|
1703
|
+
table.insert(results, {
|
|
1704
|
+
success = true,
|
|
1705
|
+
className = className,
|
|
1706
|
+
parent = parentPath,
|
|
1707
|
+
instancePath = getInstancePath(newInstance),
|
|
1708
|
+
name = newInstance.Name
|
|
1709
|
+
})
|
|
1710
|
+
else
|
|
1711
|
+
failureCount = failureCount + 1
|
|
1712
|
+
table.insert(results, {
|
|
1713
|
+
success = false,
|
|
1714
|
+
className = className,
|
|
1715
|
+
parent = parentPath,
|
|
1716
|
+
error = tostring(newInstance)
|
|
1717
|
+
})
|
|
1718
|
+
end
|
|
1719
|
+
else
|
|
1720
|
+
failureCount = failureCount + 1
|
|
1721
|
+
table.insert(results, {
|
|
1722
|
+
success = false,
|
|
1723
|
+
className = className,
|
|
1724
|
+
parent = parentPath,
|
|
1725
|
+
error = "Parent instance not found"
|
|
1726
|
+
})
|
|
1727
|
+
end
|
|
1728
|
+
else
|
|
1729
|
+
failureCount = failureCount + 1
|
|
1730
|
+
table.insert(results, {
|
|
1731
|
+
success = false,
|
|
1732
|
+
error = "Class name and parent are required"
|
|
1733
|
+
})
|
|
1734
|
+
end
|
|
1735
|
+
end
|
|
1736
|
+
|
|
1737
|
+
if successCount > 0 then
|
|
1738
|
+
ChangeHistoryService:SetWaypoint("Mass create objects")
|
|
1739
|
+
end
|
|
1740
|
+
|
|
1741
|
+
return {
|
|
1742
|
+
results = results,
|
|
1743
|
+
summary = {
|
|
1744
|
+
total = #objects,
|
|
1745
|
+
succeeded = successCount,
|
|
1746
|
+
failed = failureCount
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
end
|
|
1750
|
+
|
|
1751
|
+
handlers.massCreateObjectsWithProperties = function(requestData)
|
|
1752
|
+
local objects = requestData.objects
|
|
1753
|
+
|
|
1754
|
+
if not objects or type(objects) ~= "table" or #objects == 0 then
|
|
1755
|
+
return { error = "Objects array is required" }
|
|
1756
|
+
end
|
|
1757
|
+
|
|
1758
|
+
local results = {}
|
|
1759
|
+
local successCount = 0
|
|
1760
|
+
local failureCount = 0
|
|
1761
|
+
|
|
1762
|
+
for _, objData in ipairs(objects) do
|
|
1763
|
+
local className = objData.className
|
|
1764
|
+
local parentPath = objData.parent
|
|
1765
|
+
local name = objData.name
|
|
1766
|
+
local properties = objData.properties or {}
|
|
1767
|
+
|
|
1768
|
+
if className and parentPath then
|
|
1769
|
+
local parentInstance = getInstanceByPath(parentPath)
|
|
1770
|
+
if parentInstance then
|
|
1771
|
+
local success, newInstance = pcall(function()
|
|
1772
|
+
local instance = Instance.new(className)
|
|
1773
|
+
|
|
1774
|
+
if name then
|
|
1775
|
+
instance.Name = name
|
|
1776
|
+
end
|
|
1777
|
+
|
|
1778
|
+
-- Set Parent first so property type inference works
|
|
1779
|
+
instance.Parent = parentInstance
|
|
1780
|
+
|
|
1781
|
+
for propertyName, propertyValue in pairs(properties) do
|
|
1782
|
+
pcall(function()
|
|
1783
|
+
local convertedValue = convertPropertyValue(instance, propertyName, propertyValue)
|
|
1784
|
+
if convertedValue ~= nil then
|
|
1785
|
+
instance[propertyName] = convertedValue
|
|
1786
|
+
end
|
|
1787
|
+
end)
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
return instance
|
|
1791
|
+
end)
|
|
1792
|
+
|
|
1793
|
+
if success and newInstance then
|
|
1794
|
+
successCount = successCount + 1
|
|
1795
|
+
table.insert(results, {
|
|
1796
|
+
success = true,
|
|
1797
|
+
className = className,
|
|
1798
|
+
parent = parentPath,
|
|
1799
|
+
instancePath = getInstancePath(newInstance),
|
|
1800
|
+
name = newInstance.Name
|
|
1801
|
+
})
|
|
1802
|
+
else
|
|
1803
|
+
failureCount = failureCount + 1
|
|
1804
|
+
table.insert(results, {
|
|
1805
|
+
success = false,
|
|
1806
|
+
className = className,
|
|
1807
|
+
parent = parentPath,
|
|
1808
|
+
error = tostring(newInstance)
|
|
1809
|
+
})
|
|
1810
|
+
end
|
|
1811
|
+
else
|
|
1812
|
+
failureCount = failureCount + 1
|
|
1813
|
+
table.insert(results, {
|
|
1814
|
+
success = false,
|
|
1815
|
+
className = className,
|
|
1816
|
+
parent = parentPath,
|
|
1817
|
+
error = "Parent instance not found"
|
|
1818
|
+
})
|
|
1819
|
+
end
|
|
1820
|
+
else
|
|
1821
|
+
failureCount = failureCount + 1
|
|
1822
|
+
table.insert(results, {
|
|
1823
|
+
success = false,
|
|
1824
|
+
error = "Class name and parent are required"
|
|
1825
|
+
})
|
|
1826
|
+
end
|
|
1827
|
+
end
|
|
1828
|
+
|
|
1829
|
+
if successCount > 0 then
|
|
1830
|
+
ChangeHistoryService:SetWaypoint("Mass create objects with properties")
|
|
1831
|
+
end
|
|
1832
|
+
|
|
1833
|
+
return {
|
|
1834
|
+
results = results,
|
|
1835
|
+
summary = {
|
|
1836
|
+
total = #objects,
|
|
1837
|
+
succeeded = successCount,
|
|
1838
|
+
failed = failureCount
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
end
|
|
1842
|
+
|
|
1843
|
+
handlers.smartDuplicate = function(requestData)
|
|
1844
|
+
local instancePath = requestData.instancePath
|
|
1845
|
+
local count = requestData.count
|
|
1846
|
+
local options = requestData.options or {}
|
|
1847
|
+
|
|
1848
|
+
if not instancePath or not count or count < 1 then
|
|
1849
|
+
return { error = "Instance path and count > 0 are required" }
|
|
1850
|
+
end
|
|
1851
|
+
|
|
1852
|
+
local instance = getInstanceByPath(instancePath)
|
|
1853
|
+
if not instance then
|
|
1854
|
+
return { error = "Instance not found: " .. instancePath }
|
|
1855
|
+
end
|
|
1856
|
+
|
|
1857
|
+
local results = {}
|
|
1858
|
+
local successCount = 0
|
|
1859
|
+
local failureCount = 0
|
|
1860
|
+
|
|
1861
|
+
for i = 1, count do
|
|
1862
|
+
local success, newInstance = pcall(function()
|
|
1863
|
+
local clone = instance:Clone()
|
|
1864
|
+
|
|
1865
|
+
if options.namePattern then
|
|
1866
|
+
clone.Name = options.namePattern:gsub("{n}", tostring(i))
|
|
1867
|
+
else
|
|
1868
|
+
clone.Name = instance.Name .. i
|
|
1869
|
+
end
|
|
1870
|
+
|
|
1871
|
+
if options.positionOffset and clone:IsA("BasePart") then
|
|
1872
|
+
local offset = options.positionOffset
|
|
1873
|
+
local currentPos = clone.Position
|
|
1874
|
+
clone.Position = Vector3.new(
|
|
1875
|
+
currentPos.X + (offset[1] or 0) * i,
|
|
1876
|
+
currentPos.Y + (offset[2] or 0) * i,
|
|
1877
|
+
currentPos.Z + (offset[3] or 0) * i
|
|
1878
|
+
)
|
|
1879
|
+
end
|
|
1880
|
+
|
|
1881
|
+
if options.rotationOffset and clone:IsA("BasePart") then
|
|
1882
|
+
local offset = options.rotationOffset
|
|
1883
|
+
local currentCFrame = clone.CFrame
|
|
1884
|
+
clone.CFrame = currentCFrame * CFrame.Angles(
|
|
1885
|
+
math.rad((offset[1] or 0) * i),
|
|
1886
|
+
math.rad((offset[2] or 0) * i),
|
|
1887
|
+
math.rad((offset[3] or 0) * i)
|
|
1888
|
+
)
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
if options.scaleOffset and clone:IsA("BasePart") then
|
|
1892
|
+
local offset = options.scaleOffset
|
|
1893
|
+
local currentSize = clone.Size
|
|
1894
|
+
clone.Size = Vector3.new(
|
|
1895
|
+
currentSize.X * ((offset[1] or 1) ^ i),
|
|
1896
|
+
currentSize.Y * ((offset[2] or 1) ^ i),
|
|
1897
|
+
currentSize.Z * ((offset[3] or 1) ^ i)
|
|
1898
|
+
)
|
|
1899
|
+
end
|
|
1900
|
+
|
|
1901
|
+
if options.propertyVariations then
|
|
1902
|
+
for propName, values in pairs(options.propertyVariations) do
|
|
1903
|
+
if values and #values > 0 then
|
|
1904
|
+
local valueIndex = ((i - 1) % #values) + 1
|
|
1905
|
+
pcall(function()
|
|
1906
|
+
clone[propName] = values[valueIndex]
|
|
1907
|
+
end)
|
|
1908
|
+
end
|
|
1909
|
+
end
|
|
1910
|
+
end
|
|
1911
|
+
|
|
1912
|
+
if options.targetParents and options.targetParents[i] then
|
|
1913
|
+
local targetParent = getInstanceByPath(options.targetParents[i])
|
|
1914
|
+
if targetParent then
|
|
1915
|
+
clone.Parent = targetParent
|
|
1916
|
+
else
|
|
1917
|
+
clone.Parent = instance.Parent
|
|
1918
|
+
end
|
|
1919
|
+
else
|
|
1920
|
+
clone.Parent = instance.Parent
|
|
1921
|
+
end
|
|
1922
|
+
|
|
1923
|
+
return clone
|
|
1924
|
+
end)
|
|
1925
|
+
|
|
1926
|
+
if success and newInstance then
|
|
1927
|
+
successCount = successCount + 1
|
|
1928
|
+
table.insert(results, {
|
|
1929
|
+
success = true,
|
|
1930
|
+
instancePath = getInstancePath(newInstance),
|
|
1931
|
+
name = newInstance.Name,
|
|
1932
|
+
index = i
|
|
1933
|
+
})
|
|
1934
|
+
else
|
|
1935
|
+
failureCount = failureCount + 1
|
|
1936
|
+
table.insert(results, {
|
|
1937
|
+
success = false,
|
|
1938
|
+
index = i,
|
|
1939
|
+
error = tostring(newInstance)
|
|
1940
|
+
})
|
|
1941
|
+
end
|
|
1942
|
+
end
|
|
1943
|
+
|
|
1944
|
+
if successCount > 0 then
|
|
1945
|
+
ChangeHistoryService:SetWaypoint("Smart duplicate " .. instance.Name .. " (" .. successCount .. " copies)")
|
|
1946
|
+
end
|
|
1947
|
+
|
|
1948
|
+
return {
|
|
1949
|
+
results = results,
|
|
1950
|
+
summary = {
|
|
1951
|
+
total = count,
|
|
1952
|
+
succeeded = successCount,
|
|
1953
|
+
failed = failureCount
|
|
1954
|
+
},
|
|
1955
|
+
sourceInstance = instancePath
|
|
1956
|
+
}
|
|
1957
|
+
end
|
|
1958
|
+
|
|
1959
|
+
handlers.massDuplicate = function(requestData)
|
|
1960
|
+
local duplications = requestData.duplications
|
|
1961
|
+
|
|
1962
|
+
if not duplications or type(duplications) ~= "table" or #duplications == 0 then
|
|
1963
|
+
return { error = "Duplications array is required" }
|
|
1964
|
+
end
|
|
1965
|
+
|
|
1966
|
+
local allResults = {}
|
|
1967
|
+
local totalSuccess = 0
|
|
1968
|
+
local totalFailures = 0
|
|
1969
|
+
|
|
1970
|
+
for _, duplication in ipairs(duplications) do
|
|
1971
|
+
local result = handlers.smartDuplicate(duplication)
|
|
1972
|
+
table.insert(allResults, result)
|
|
1973
|
+
|
|
1974
|
+
if result.summary then
|
|
1975
|
+
totalSuccess = totalSuccess + result.summary.succeeded
|
|
1976
|
+
totalFailures = totalFailures + result.summary.failed
|
|
1977
|
+
end
|
|
1978
|
+
end
|
|
1979
|
+
|
|
1980
|
+
if totalSuccess > 0 then
|
|
1981
|
+
ChangeHistoryService:SetWaypoint("Mass duplicate operations (" .. totalSuccess .. " objects)")
|
|
1982
|
+
end
|
|
1983
|
+
|
|
1984
|
+
return {
|
|
1985
|
+
results = allResults,
|
|
1986
|
+
summary = {
|
|
1987
|
+
total = totalSuccess + totalFailures,
|
|
1988
|
+
succeeded = totalSuccess,
|
|
1989
|
+
failed = totalFailures
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
end
|
|
1993
|
+
|
|
1994
|
+
local function evaluateFormula(formula, variables, instance, index)
|
|
1995
|
+
|
|
1996
|
+
local value = formula
|
|
1997
|
+
|
|
1998
|
+
value = value:gsub("index", tostring(index))
|
|
1999
|
+
|
|
2000
|
+
if instance and instance:IsA("BasePart") then
|
|
2001
|
+
local pos = instance.Position
|
|
2002
|
+
local size = instance.Size
|
|
2003
|
+
value = value:gsub("Position%.X", tostring(pos.X))
|
|
2004
|
+
value = value:gsub("Position%.Y", tostring(pos.Y))
|
|
2005
|
+
value = value:gsub("Position%.Z", tostring(pos.Z))
|
|
2006
|
+
value = value:gsub("Size%.X", tostring(size.X))
|
|
2007
|
+
value = value:gsub("Size%.Y", tostring(size.Y))
|
|
2008
|
+
value = value:gsub("Size%.Z", tostring(size.Z))
|
|
2009
|
+
value = value:gsub("magnitude", tostring(pos.magnitude))
|
|
2010
|
+
end
|
|
2011
|
+
|
|
2012
|
+
if variables then
|
|
2013
|
+
for k, v in pairs(variables) do
|
|
2014
|
+
value = value:gsub(k, tostring(v))
|
|
2015
|
+
end
|
|
2016
|
+
end
|
|
2017
|
+
|
|
2018
|
+
value = value:gsub("sin%(([%d%.%-]+)%)", function(x) return tostring(math.sin(tonumber(x) or 0)) end)
|
|
2019
|
+
value = value:gsub("cos%(([%d%.%-]+)%)", function(x) return tostring(math.cos(tonumber(x) or 0)) end)
|
|
2020
|
+
value = value:gsub("sqrt%(([%d%.%-]+)%)", function(x) return tostring(math.sqrt(tonumber(x) or 0)) end)
|
|
2021
|
+
value = value:gsub("abs%(([%d%.%-]+)%)", function(x) return tostring(math.abs(tonumber(x) or 0)) end)
|
|
2022
|
+
value = value:gsub("floor%(([%d%.%-]+)%)", function(x) return tostring(math.floor(tonumber(x) or 0)) end)
|
|
2023
|
+
value = value:gsub("ceil%(([%d%.%-]+)%)", function(x) return tostring(math.ceil(tonumber(x) or 0)) end)
|
|
2024
|
+
|
|
2025
|
+
local result = tonumber(value)
|
|
2026
|
+
if result then
|
|
2027
|
+
return result, nil
|
|
2028
|
+
end
|
|
2029
|
+
|
|
2030
|
+
local success, evalResult = pcall(function()
|
|
2031
|
+
|
|
2032
|
+
local num = tonumber(value)
|
|
2033
|
+
if num then
|
|
2034
|
+
return num
|
|
2035
|
+
end
|
|
2036
|
+
|
|
2037
|
+
local a, b = value:match("^([%d%.%-]+)%s*%*%s*([%d%.%-]+)$")
|
|
2038
|
+
if a and b then
|
|
2039
|
+
return (tonumber(a) or 0) * (tonumber(b) or 0)
|
|
2040
|
+
end
|
|
2041
|
+
|
|
2042
|
+
a, b = value:match("^([%d%.%-]+)%s*%+%s*([%d%.%-]+)$")
|
|
2043
|
+
if a and b then
|
|
2044
|
+
return (tonumber(a) or 0) + (tonumber(b) or 0)
|
|
2045
|
+
end
|
|
2046
|
+
|
|
2047
|
+
a, b = value:match("^([%d%.%-]+)%s*%-%s*([%d%.%-]+)$")
|
|
2048
|
+
if a and b then
|
|
2049
|
+
return (tonumber(a) or 0) - (tonumber(b) or 0)
|
|
2050
|
+
end
|
|
2051
|
+
|
|
2052
|
+
a, b = value:match("^([%d%.%-]+)%s*/%s*([%d%.%-]+)$")
|
|
2053
|
+
if a and b then
|
|
2054
|
+
local divisor = tonumber(b) or 1
|
|
2055
|
+
if divisor ~= 0 then
|
|
2056
|
+
return (tonumber(a) or 0) / divisor
|
|
2057
|
+
end
|
|
2058
|
+
end
|
|
2059
|
+
|
|
2060
|
+
error("Unsupported formula pattern: " .. value)
|
|
2061
|
+
end)
|
|
2062
|
+
|
|
2063
|
+
if success and type(evalResult) == "number" then
|
|
2064
|
+
return evalResult, nil
|
|
2065
|
+
else
|
|
2066
|
+
return index, "Complex formulas not supported - using index value"
|
|
2067
|
+
end
|
|
2068
|
+
end
|
|
2069
|
+
|
|
2070
|
+
handlers.setCalculatedProperty = function(requestData)
|
|
2071
|
+
local paths = requestData.paths
|
|
2072
|
+
local propertyName = requestData.propertyName
|
|
2073
|
+
local formula = requestData.formula
|
|
2074
|
+
local variables = requestData.variables
|
|
2075
|
+
|
|
2076
|
+
if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName or not formula then
|
|
2077
|
+
return { error = "Paths, property name, and formula are required" }
|
|
2078
|
+
end
|
|
2079
|
+
|
|
2080
|
+
local results = {}
|
|
2081
|
+
local successCount = 0
|
|
2082
|
+
local failureCount = 0
|
|
2083
|
+
|
|
2084
|
+
for index, path in ipairs(paths) do
|
|
2085
|
+
local instance = getInstanceByPath(path)
|
|
2086
|
+
if instance then
|
|
2087
|
+
local value, evalError = evaluateFormula(formula, variables, instance, index)
|
|
2088
|
+
|
|
2089
|
+
if value ~= nil and not evalError then
|
|
2090
|
+
local success, err = pcall(function()
|
|
2091
|
+
instance[propertyName] = value
|
|
2092
|
+
end)
|
|
2093
|
+
|
|
2094
|
+
if success then
|
|
2095
|
+
successCount = successCount + 1
|
|
2096
|
+
table.insert(results, {
|
|
2097
|
+
path = path,
|
|
2098
|
+
success = true,
|
|
2099
|
+
propertyName = propertyName,
|
|
2100
|
+
calculatedValue = value,
|
|
2101
|
+
formula = formula
|
|
2102
|
+
})
|
|
2103
|
+
else
|
|
2104
|
+
failureCount = failureCount + 1
|
|
2105
|
+
table.insert(results, {
|
|
2106
|
+
path = path,
|
|
2107
|
+
success = false,
|
|
2108
|
+
error = "Property set failed: " .. tostring(err)
|
|
2109
|
+
})
|
|
2110
|
+
end
|
|
2111
|
+
else
|
|
2112
|
+
failureCount = failureCount + 1
|
|
2113
|
+
table.insert(results, {
|
|
2114
|
+
path = path,
|
|
2115
|
+
success = false,
|
|
2116
|
+
error = evalError or "Formula evaluation failed"
|
|
2117
|
+
})
|
|
2118
|
+
end
|
|
2119
|
+
else
|
|
2120
|
+
failureCount = failureCount + 1
|
|
2121
|
+
table.insert(results, {
|
|
2122
|
+
path = path,
|
|
2123
|
+
success = false,
|
|
2124
|
+
error = "Instance not found"
|
|
2125
|
+
})
|
|
2126
|
+
end
|
|
2127
|
+
end
|
|
2128
|
+
|
|
2129
|
+
if successCount > 0 then
|
|
2130
|
+
ChangeHistoryService:SetWaypoint("Set calculated " .. propertyName .. " property")
|
|
2131
|
+
end
|
|
2132
|
+
|
|
2133
|
+
return {
|
|
2134
|
+
results = results,
|
|
2135
|
+
summary = {
|
|
2136
|
+
total = #paths,
|
|
2137
|
+
succeeded = successCount,
|
|
2138
|
+
failed = failureCount
|
|
2139
|
+
},
|
|
2140
|
+
formula = formula
|
|
2141
|
+
}
|
|
2142
|
+
end
|
|
2143
|
+
|
|
2144
|
+
handlers.setRelativeProperty = function(requestData)
|
|
2145
|
+
local paths = requestData.paths
|
|
2146
|
+
local propertyName = requestData.propertyName
|
|
2147
|
+
local operation = requestData.operation
|
|
2148
|
+
local value = requestData.value
|
|
2149
|
+
local component = requestData.component
|
|
2150
|
+
|
|
2151
|
+
if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName or not operation or value == nil then
|
|
2152
|
+
return { error = "Paths, property name, operation, and value are required" }
|
|
2153
|
+
end
|
|
2154
|
+
|
|
2155
|
+
local results = {}
|
|
2156
|
+
local successCount = 0
|
|
2157
|
+
local failureCount = 0
|
|
2158
|
+
|
|
2159
|
+
for _, path in ipairs(paths) do
|
|
2160
|
+
local instance = getInstanceByPath(path)
|
|
2161
|
+
if instance then
|
|
2162
|
+
local success, err = pcall(function()
|
|
2163
|
+
local currentValue = instance[propertyName]
|
|
2164
|
+
local newValue
|
|
2165
|
+
|
|
2166
|
+
if component and typeof(currentValue) == "Vector3" then
|
|
2167
|
+
local x, y, z = currentValue.X, currentValue.Y, currentValue.Z
|
|
2168
|
+
local targetValue = value
|
|
2169
|
+
|
|
2170
|
+
if component == "X" then
|
|
2171
|
+
if operation == "add" then x = x + targetValue
|
|
2172
|
+
elseif operation == "subtract" then x = x - targetValue
|
|
2173
|
+
elseif operation == "multiply" then x = x * targetValue
|
|
2174
|
+
elseif operation == "divide" then x = x / targetValue
|
|
2175
|
+
elseif operation == "power" then x = x ^ targetValue
|
|
2176
|
+
end
|
|
2177
|
+
elseif component == "Y" then
|
|
2178
|
+
if operation == "add" then y = y + targetValue
|
|
2179
|
+
elseif operation == "subtract" then y = y - targetValue
|
|
2180
|
+
elseif operation == "multiply" then y = y * targetValue
|
|
2181
|
+
elseif operation == "divide" then y = y / targetValue
|
|
2182
|
+
elseif operation == "power" then y = y ^ targetValue
|
|
2183
|
+
end
|
|
2184
|
+
elseif component == "Z" then
|
|
2185
|
+
if operation == "add" then z = z + targetValue
|
|
2186
|
+
elseif operation == "subtract" then z = z - targetValue
|
|
2187
|
+
elseif operation == "multiply" then z = z * targetValue
|
|
2188
|
+
elseif operation == "divide" then z = z / targetValue
|
|
2189
|
+
elseif operation == "power" then z = z ^ targetValue
|
|
2190
|
+
end
|
|
2191
|
+
end
|
|
2192
|
+
|
|
2193
|
+
newValue = Vector3.new(x, y, z)
|
|
2194
|
+
elseif typeof(currentValue) == "Color3" and typeof(value) == "Color3" then
|
|
2195
|
+
local r, g, b = currentValue.R, currentValue.G, currentValue.B
|
|
2196
|
+
|
|
2197
|
+
if operation == "add" then
|
|
2198
|
+
newValue = Color3.new(
|
|
2199
|
+
math.min(1, r + value.R),
|
|
2200
|
+
math.min(1, g + value.G),
|
|
2201
|
+
math.min(1, b + value.B)
|
|
2202
|
+
)
|
|
2203
|
+
elseif operation == "subtract" then
|
|
2204
|
+
newValue = Color3.new(
|
|
2205
|
+
math.max(0, r - value.R),
|
|
2206
|
+
math.max(0, g - value.G),
|
|
2207
|
+
math.max(0, b - value.B)
|
|
2208
|
+
)
|
|
2209
|
+
elseif operation == "multiply" then
|
|
2210
|
+
newValue = Color3.new(r * value.R, g * value.G, b * value.B)
|
|
2211
|
+
end
|
|
2212
|
+
elseif type(currentValue) == "number" and type(value) == "number" then
|
|
2213
|
+
if operation == "add" then
|
|
2214
|
+
newValue = currentValue + value
|
|
2215
|
+
elseif operation == "subtract" then
|
|
2216
|
+
newValue = currentValue - value
|
|
2217
|
+
elseif operation == "multiply" then
|
|
2218
|
+
newValue = currentValue * value
|
|
2219
|
+
elseif operation == "divide" then
|
|
2220
|
+
newValue = currentValue / value
|
|
2221
|
+
elseif operation == "power" then
|
|
2222
|
+
newValue = currentValue ^ value
|
|
2223
|
+
end
|
|
2224
|
+
elseif typeof(currentValue) == "Vector3" and type(value) == "number" then
|
|
2225
|
+
local x, y, z = currentValue.X, currentValue.Y, currentValue.Z
|
|
2226
|
+
|
|
2227
|
+
if operation == "add" then
|
|
2228
|
+
newValue = Vector3.new(x + value, y + value, z + value)
|
|
2229
|
+
elseif operation == "subtract" then
|
|
2230
|
+
newValue = Vector3.new(x - value, y - value, z - value)
|
|
2231
|
+
elseif operation == "multiply" then
|
|
2232
|
+
newValue = Vector3.new(x * value, y * value, z * value)
|
|
2233
|
+
elseif operation == "divide" then
|
|
2234
|
+
newValue = Vector3.new(x / value, y / value, z / value)
|
|
2235
|
+
elseif operation == "power" then
|
|
2236
|
+
newValue = Vector3.new(x ^ value, y ^ value, z ^ value)
|
|
2237
|
+
end
|
|
2238
|
+
else
|
|
2239
|
+
error("Unsupported property type or operation")
|
|
2240
|
+
end
|
|
2241
|
+
|
|
2242
|
+
instance[propertyName] = newValue
|
|
2243
|
+
return newValue
|
|
2244
|
+
end)
|
|
2245
|
+
|
|
2246
|
+
if success then
|
|
2247
|
+
successCount = successCount + 1
|
|
2248
|
+
table.insert(results, {
|
|
2249
|
+
path = path,
|
|
2250
|
+
success = true,
|
|
2251
|
+
propertyName = propertyName,
|
|
2252
|
+
operation = operation,
|
|
2253
|
+
value = value,
|
|
2254
|
+
component = component,
|
|
2255
|
+
newValue = err
|
|
2256
|
+
})
|
|
2257
|
+
else
|
|
2258
|
+
failureCount = failureCount + 1
|
|
2259
|
+
table.insert(results, {
|
|
2260
|
+
path = path,
|
|
2261
|
+
success = false,
|
|
2262
|
+
error = tostring(err)
|
|
2263
|
+
})
|
|
2264
|
+
end
|
|
2265
|
+
else
|
|
2266
|
+
failureCount = failureCount + 1
|
|
2267
|
+
table.insert(results, {
|
|
2268
|
+
path = path,
|
|
2269
|
+
success = false,
|
|
2270
|
+
error = "Instance not found"
|
|
2271
|
+
})
|
|
2272
|
+
end
|
|
2273
|
+
end
|
|
2274
|
+
|
|
2275
|
+
if successCount > 0 then
|
|
2276
|
+
ChangeHistoryService:SetWaypoint("Set relative " .. propertyName .. " property")
|
|
2277
|
+
end
|
|
2278
|
+
|
|
2279
|
+
return {
|
|
2280
|
+
results = results,
|
|
2281
|
+
summary = {
|
|
2282
|
+
total = #paths,
|
|
2283
|
+
succeeded = successCount,
|
|
2284
|
+
failed = failureCount
|
|
2285
|
+
},
|
|
2286
|
+
operation = operation,
|
|
2287
|
+
value = value
|
|
2288
|
+
}
|
|
2289
|
+
end
|
|
2290
|
+
|
|
2291
|
+
handlers.getScriptSource = function(requestData)
|
|
2292
|
+
local instancePath = requestData.instancePath
|
|
2293
|
+
local startLine = requestData.startLine
|
|
2294
|
+
local endLine = requestData.endLine
|
|
2295
|
+
|
|
2296
|
+
if not instancePath then
|
|
2297
|
+
return { error = "Instance path is required" }
|
|
2298
|
+
end
|
|
2299
|
+
|
|
2300
|
+
local instance = getInstanceByPath(instancePath)
|
|
2301
|
+
if not instance then
|
|
2302
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2303
|
+
end
|
|
2304
|
+
|
|
2305
|
+
if not instance:IsA("LuaSourceContainer") then
|
|
2306
|
+
return { error = "Instance is not a script-like object: " .. instance.ClassName }
|
|
2307
|
+
end
|
|
2308
|
+
|
|
2309
|
+
local success, result = pcall(function()
|
|
2310
|
+
local fullSource = instance.Source
|
|
2311
|
+
local lines, hasTrailingNewline = splitLines(fullSource)
|
|
2312
|
+
local totalLineCount = #lines
|
|
2313
|
+
|
|
2314
|
+
-- If line range is specified, extract only those lines
|
|
2315
|
+
local sourceToReturn = fullSource
|
|
2316
|
+
local returnedStartLine = 1
|
|
2317
|
+
local returnedEndLine = totalLineCount
|
|
2318
|
+
|
|
2319
|
+
if startLine or endLine then
|
|
2320
|
+
local actualStartLine = math.max(1, startLine or 1)
|
|
2321
|
+
local actualEndLine = math.min(#lines, endLine or #lines)
|
|
2322
|
+
|
|
2323
|
+
local selectedLines = {}
|
|
2324
|
+
for i = actualStartLine, actualEndLine do
|
|
2325
|
+
table.insert(selectedLines, lines[i] or "")
|
|
2326
|
+
end
|
|
2327
|
+
|
|
2328
|
+
sourceToReturn = table.concat(selectedLines, '\n')
|
|
2329
|
+
if hasTrailingNewline and actualEndLine == #lines and sourceToReturn:sub(-1) ~= "\n" then
|
|
2330
|
+
sourceToReturn ..= "\n"
|
|
2331
|
+
end
|
|
2332
|
+
returnedStartLine = actualStartLine
|
|
2333
|
+
returnedEndLine = actualEndLine
|
|
2334
|
+
end
|
|
2335
|
+
|
|
2336
|
+
-- Build numbered source for AI agents to accurately identify line numbers
|
|
2337
|
+
local numberedLines = {}
|
|
2338
|
+
local linesToNumber = startLine and select(1, splitLines(sourceToReturn)) or lines
|
|
2339
|
+
local lineOffset = returnedStartLine - 1
|
|
2340
|
+
for i, line in ipairs(linesToNumber) do
|
|
2341
|
+
table.insert(numberedLines, (i + lineOffset) .. ": " .. line)
|
|
2342
|
+
end
|
|
2343
|
+
local numberedSource = table.concat(numberedLines, "\n")
|
|
2344
|
+
|
|
2345
|
+
local resp = {
|
|
2346
|
+
instancePath = instancePath,
|
|
2347
|
+
className = instance.ClassName,
|
|
2348
|
+
name = instance.Name,
|
|
2349
|
+
source = sourceToReturn,
|
|
2350
|
+
numberedSource = numberedSource,
|
|
2351
|
+
sourceLength = string.len(fullSource),
|
|
2352
|
+
lineCount = totalLineCount,
|
|
2353
|
+
-- Line range info
|
|
2354
|
+
startLine = returnedStartLine,
|
|
2355
|
+
endLine = returnedEndLine,
|
|
2356
|
+
isPartial = (startLine ~= nil or endLine ~= nil),
|
|
2357
|
+
-- Helpful metadata for large scripts
|
|
2358
|
+
truncated = false,
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
-- If the source is very large (>50000 chars) and no range specified,
|
|
2362
|
+
-- return first 1000 lines with truncation notice
|
|
2363
|
+
if not startLine and not endLine and string.len(fullSource) > 50000 then
|
|
2364
|
+
local truncatedLines = {}
|
|
2365
|
+
local truncatedNumberedLines = {}
|
|
2366
|
+
local maxLines = math.min(1000, #lines)
|
|
2367
|
+
for i = 1, maxLines do
|
|
2368
|
+
table.insert(truncatedLines, lines[i])
|
|
2369
|
+
table.insert(truncatedNumberedLines, i .. ": " .. lines[i])
|
|
2370
|
+
end
|
|
2371
|
+
resp.source = table.concat(truncatedLines, '\n')
|
|
2372
|
+
resp.numberedSource = table.concat(truncatedNumberedLines, '\n')
|
|
2373
|
+
resp.truncated = true
|
|
2374
|
+
resp.endLine = maxLines
|
|
2375
|
+
resp.note = "Script truncated to first 1000 lines. Use startLine/endLine parameters to read specific sections."
|
|
2376
|
+
end
|
|
2377
|
+
|
|
2378
|
+
if instance:IsA("BaseScript") then
|
|
2379
|
+
resp.enabled = instance.Enabled
|
|
2380
|
+
end
|
|
2381
|
+
return resp
|
|
2382
|
+
end)
|
|
2383
|
+
|
|
2384
|
+
if success then
|
|
2385
|
+
return result
|
|
2386
|
+
else
|
|
2387
|
+
return { error = "Failed to get script source: " .. tostring(result) }
|
|
2388
|
+
end
|
|
2389
|
+
end
|
|
2390
|
+
|
|
2391
|
+
handlers.setScriptSource = function(requestData)
|
|
2392
|
+
local instancePath = requestData.instancePath
|
|
2393
|
+
local newSource = requestData.source
|
|
2394
|
+
|
|
2395
|
+
if not instancePath or not newSource then
|
|
2396
|
+
return { error = "Instance path and source are required" }
|
|
2397
|
+
end
|
|
2398
|
+
|
|
2399
|
+
local instance = getInstanceByPath(instancePath)
|
|
2400
|
+
if not instance then
|
|
2401
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2402
|
+
end
|
|
2403
|
+
|
|
2404
|
+
if not instance:IsA("LuaSourceContainer") then
|
|
2405
|
+
return { error = "Instance is not a script-like object: " .. instance.ClassName }
|
|
2406
|
+
end
|
|
2407
|
+
|
|
2408
|
+
-- Normalize escape sequences that may have been double-escaped
|
|
2409
|
+
local sourceToSet = newSource :: string
|
|
2410
|
+
-- Fix double-escaped newlines, tabs, etc.
|
|
2411
|
+
sourceToSet = sourceToSet:gsub("\\n", "\n")
|
|
2412
|
+
sourceToSet = sourceToSet:gsub("\\t", "\t")
|
|
2413
|
+
sourceToSet = sourceToSet:gsub("\\r", "\r")
|
|
2414
|
+
sourceToSet = sourceToSet:gsub("\\\\", "\\")
|
|
2415
|
+
local updateSuccess, updateResult = pcall(function()
|
|
2416
|
+
local oldSourceLength = string.len(instance.Source)
|
|
2417
|
+
|
|
2418
|
+
ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
|
|
2419
|
+
return sourceToSet
|
|
2420
|
+
end)
|
|
2421
|
+
|
|
2422
|
+
ChangeHistoryService:SetWaypoint("Set script source: " .. instance.Name)
|
|
2423
|
+
|
|
2424
|
+
return {
|
|
2425
|
+
success = true,
|
|
2426
|
+
instancePath = instancePath,
|
|
2427
|
+
oldSourceLength = oldSourceLength,
|
|
2428
|
+
newSourceLength = string.len(sourceToSet),
|
|
2429
|
+
method = "UpdateSourceAsync",
|
|
2430
|
+
message = "Script source updated successfully (editor-safe)"
|
|
2431
|
+
}
|
|
2432
|
+
end)
|
|
2433
|
+
|
|
2434
|
+
if updateSuccess then
|
|
2435
|
+
return updateResult
|
|
2436
|
+
end
|
|
2437
|
+
|
|
2438
|
+
-- Fallback to direct assignment if UpdateSourceAsync fails
|
|
2439
|
+
local directSuccess, directResult = pcall(function()
|
|
2440
|
+
local oldSource = instance.Source
|
|
2441
|
+
instance.Source = sourceToSet
|
|
2442
|
+
|
|
2443
|
+
ChangeHistoryService:SetWaypoint("Set script source: " .. instance.Name)
|
|
2444
|
+
|
|
2445
|
+
return {
|
|
2446
|
+
success = true,
|
|
2447
|
+
instancePath = instancePath,
|
|
2448
|
+
oldSourceLength = string.len(oldSource),
|
|
2449
|
+
newSourceLength = string.len(sourceToSet),
|
|
2450
|
+
method = "direct",
|
|
2451
|
+
message = "Script source updated successfully (direct assignment)"
|
|
2452
|
+
}
|
|
2453
|
+
end)
|
|
2454
|
+
|
|
2455
|
+
if directSuccess then
|
|
2456
|
+
return directResult
|
|
2457
|
+
end
|
|
2458
|
+
|
|
2459
|
+
-- Final fallback: replace the script entirely
|
|
2460
|
+
local replaceSuccess, replaceResult = pcall(function()
|
|
2461
|
+
local parent = instance.Parent
|
|
2462
|
+
local name = instance.Name
|
|
2463
|
+
local className = instance.ClassName
|
|
2464
|
+
local wasBaseScript = instance:IsA("BaseScript")
|
|
2465
|
+
local enabled
|
|
2466
|
+
if wasBaseScript then
|
|
2467
|
+
enabled = instance.Enabled
|
|
2468
|
+
end
|
|
2469
|
+
|
|
2470
|
+
local newScript = Instance.new(className)
|
|
2471
|
+
newScript.Name = name
|
|
2472
|
+
newScript.Source = sourceToSet
|
|
2473
|
+
if wasBaseScript then
|
|
2474
|
+
newScript.Enabled = enabled
|
|
2475
|
+
end
|
|
2476
|
+
|
|
2477
|
+
newScript.Parent = parent
|
|
2478
|
+
instance:Destroy()
|
|
2479
|
+
|
|
2480
|
+
ChangeHistoryService:SetWaypoint("Replace script: " .. name)
|
|
2481
|
+
|
|
2482
|
+
return {
|
|
2483
|
+
success = true,
|
|
2484
|
+
instancePath = getInstancePath(newScript),
|
|
2485
|
+
method = "replace",
|
|
2486
|
+
message = "Script replaced successfully with new source"
|
|
2487
|
+
}
|
|
2488
|
+
end)
|
|
2489
|
+
|
|
2490
|
+
if replaceSuccess then
|
|
2491
|
+
return replaceResult
|
|
2492
|
+
else
|
|
2493
|
+
return {
|
|
2494
|
+
error = "Failed to set script source. UpdateSourceAsync failed: " .. tostring(updateResult) ..
|
|
2495
|
+
". Direct assignment failed: " .. tostring(directResult) ..
|
|
2496
|
+
". Replace method failed: " .. tostring(replaceResult)
|
|
2497
|
+
}
|
|
2498
|
+
end
|
|
2499
|
+
end
|
|
2500
|
+
|
|
2501
|
+
-- Partial Script Editing: Edit specific lines
|
|
2502
|
+
handlers.editScriptLines = function(requestData)
|
|
2503
|
+
local instancePath = requestData.instancePath
|
|
2504
|
+
local startLine = requestData.startLine
|
|
2505
|
+
local endLine = requestData.endLine
|
|
2506
|
+
local newContent = requestData.newContent
|
|
2507
|
+
|
|
2508
|
+
if not instancePath or not startLine or not endLine or not newContent then
|
|
2509
|
+
return { error = "Instance path, startLine, endLine, and newContent are required" }
|
|
2510
|
+
end
|
|
2511
|
+
|
|
2512
|
+
-- Normalize escape sequences that may have been double-escaped
|
|
2513
|
+
newContent = newContent:gsub("\\n", "\n")
|
|
2514
|
+
newContent = newContent:gsub("\\t", "\t")
|
|
2515
|
+
newContent = newContent:gsub("\\r", "\r")
|
|
2516
|
+
newContent = newContent:gsub("\\\\", "\\")
|
|
2517
|
+
|
|
2518
|
+
local instance = getInstanceByPath(instancePath)
|
|
2519
|
+
if not instance then
|
|
2520
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2521
|
+
end
|
|
2522
|
+
|
|
2523
|
+
if not instance:IsA("LuaSourceContainer") then
|
|
2524
|
+
return { error = "Instance is not a script-like object: " .. instance.ClassName }
|
|
2525
|
+
end
|
|
2526
|
+
|
|
2527
|
+
local success, result = pcall(function()
|
|
2528
|
+
local lines, hadTrailingNewline = splitLines(instance.Source)
|
|
2529
|
+
local totalLines = #lines
|
|
2530
|
+
|
|
2531
|
+
if startLine < 1 or startLine > totalLines then
|
|
2532
|
+
error("startLine out of range (1-" .. totalLines .. ")")
|
|
2533
|
+
end
|
|
2534
|
+
if endLine < startLine or endLine > totalLines then
|
|
2535
|
+
error("endLine out of range (" .. startLine .. "-" .. totalLines .. ")")
|
|
2536
|
+
end
|
|
2537
|
+
|
|
2538
|
+
-- Split new content into lines
|
|
2539
|
+
local newLines = select(1, splitLines(newContent))
|
|
2540
|
+
|
|
2541
|
+
-- Build new source: lines before + new content + lines after
|
|
2542
|
+
local resultLines = {}
|
|
2543
|
+
|
|
2544
|
+
-- Lines before the edit
|
|
2545
|
+
for i = 1, startLine - 1 do
|
|
2546
|
+
table.insert(resultLines, lines[i])
|
|
2547
|
+
end
|
|
2548
|
+
|
|
2549
|
+
-- New content lines
|
|
2550
|
+
for _, line in ipairs(newLines) do
|
|
2551
|
+
table.insert(resultLines, line)
|
|
2552
|
+
end
|
|
2553
|
+
|
|
2554
|
+
-- Lines after the edit
|
|
2555
|
+
for i = endLine + 1, totalLines do
|
|
2556
|
+
table.insert(resultLines, lines[i])
|
|
2557
|
+
end
|
|
2558
|
+
|
|
2559
|
+
local newSource = joinLines(resultLines, hadTrailingNewline)
|
|
2560
|
+
|
|
2561
|
+
-- Use UpdateSourceAsync for editor compatibility
|
|
2562
|
+
ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
|
|
2563
|
+
return newSource
|
|
2564
|
+
end)
|
|
2565
|
+
|
|
2566
|
+
ChangeHistoryService:SetWaypoint("Edit script lines " .. startLine .. "-" .. endLine .. ": " .. instance.Name)
|
|
2567
|
+
|
|
2568
|
+
return {
|
|
2569
|
+
success = true,
|
|
2570
|
+
instancePath = instancePath,
|
|
2571
|
+
editedLines = { startLine = startLine, endLine = endLine },
|
|
2572
|
+
linesRemoved = endLine - startLine + 1,
|
|
2573
|
+
linesAdded = #newLines,
|
|
2574
|
+
newLineCount = #resultLines,
|
|
2575
|
+
message = "Script lines edited successfully"
|
|
2576
|
+
}
|
|
2577
|
+
end)
|
|
2578
|
+
|
|
2579
|
+
if success then
|
|
2580
|
+
return result
|
|
2581
|
+
else
|
|
2582
|
+
return { error = "Failed to edit script lines: " .. tostring(result) }
|
|
2583
|
+
end
|
|
2584
|
+
end
|
|
2585
|
+
|
|
2586
|
+
-- Partial Script Editing: Insert lines after a specific line
|
|
2587
|
+
handlers.insertScriptLines = function(requestData)
|
|
2588
|
+
local instancePath = requestData.instancePath
|
|
2589
|
+
local afterLine = requestData.afterLine or 0 -- 0 means insert at beginning
|
|
2590
|
+
local newContent = requestData.newContent
|
|
2591
|
+
|
|
2592
|
+
if not instancePath or not newContent then
|
|
2593
|
+
return { error = "Instance path and newContent are required" }
|
|
2594
|
+
end
|
|
2595
|
+
|
|
2596
|
+
-- Normalize escape sequences that may have been double-escaped
|
|
2597
|
+
newContent = newContent:gsub("\\n", "\n")
|
|
2598
|
+
newContent = newContent:gsub("\\t", "\t")
|
|
2599
|
+
newContent = newContent:gsub("\\r", "\r")
|
|
2600
|
+
newContent = newContent:gsub("\\\\", "\\")
|
|
2601
|
+
|
|
2602
|
+
local instance = getInstanceByPath(instancePath)
|
|
2603
|
+
if not instance then
|
|
2604
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2605
|
+
end
|
|
2606
|
+
|
|
2607
|
+
if not instance:IsA("LuaSourceContainer") then
|
|
2608
|
+
return { error = "Instance is not a script-like object: " .. instance.ClassName }
|
|
2609
|
+
end
|
|
2610
|
+
|
|
2611
|
+
local success, result = pcall(function()
|
|
2612
|
+
local lines, hadTrailingNewline = splitLines(instance.Source)
|
|
2613
|
+
local totalLines = #lines
|
|
2614
|
+
|
|
2615
|
+
if afterLine < 0 or afterLine > totalLines then
|
|
2616
|
+
error("afterLine out of range (0-" .. totalLines .. ")")
|
|
2617
|
+
end
|
|
2618
|
+
|
|
2619
|
+
-- Split new content into lines
|
|
2620
|
+
local newLines = select(1, splitLines(newContent))
|
|
2621
|
+
|
|
2622
|
+
-- Build new source
|
|
2623
|
+
local resultLines = {}
|
|
2624
|
+
|
|
2625
|
+
-- Lines before insertion point
|
|
2626
|
+
for i = 1, afterLine do
|
|
2627
|
+
table.insert(resultLines, lines[i])
|
|
2628
|
+
end
|
|
2629
|
+
|
|
2630
|
+
-- New content lines
|
|
2631
|
+
for _, line in ipairs(newLines) do
|
|
2632
|
+
table.insert(resultLines, line)
|
|
2633
|
+
end
|
|
2634
|
+
|
|
2635
|
+
-- Lines after insertion point
|
|
2636
|
+
for i = afterLine + 1, totalLines do
|
|
2637
|
+
table.insert(resultLines, lines[i])
|
|
2638
|
+
end
|
|
2639
|
+
|
|
2640
|
+
local newSource = joinLines(resultLines, hadTrailingNewline)
|
|
2641
|
+
|
|
2642
|
+
-- Use UpdateSourceAsync for editor compatibility
|
|
2643
|
+
ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
|
|
2644
|
+
return newSource
|
|
2645
|
+
end)
|
|
2646
|
+
|
|
2647
|
+
ChangeHistoryService:SetWaypoint("Insert script lines after line " .. afterLine .. ": " .. instance.Name)
|
|
2648
|
+
|
|
2649
|
+
return {
|
|
2650
|
+
success = true,
|
|
2651
|
+
instancePath = instancePath,
|
|
2652
|
+
insertedAfterLine = afterLine,
|
|
2653
|
+
linesInserted = #newLines,
|
|
2654
|
+
newLineCount = #resultLines,
|
|
2655
|
+
message = "Script lines inserted successfully"
|
|
2656
|
+
}
|
|
2657
|
+
end)
|
|
2658
|
+
|
|
2659
|
+
if success then
|
|
2660
|
+
return result
|
|
2661
|
+
else
|
|
2662
|
+
return { error = "Failed to insert script lines: " .. tostring(result) }
|
|
2663
|
+
end
|
|
2664
|
+
end
|
|
2665
|
+
|
|
2666
|
+
-- Partial Script Editing: Delete specific lines
|
|
2667
|
+
handlers.deleteScriptLines = function(requestData)
|
|
2668
|
+
local instancePath = requestData.instancePath
|
|
2669
|
+
local startLine = requestData.startLine
|
|
2670
|
+
local endLine = requestData.endLine
|
|
2671
|
+
|
|
2672
|
+
if not instancePath or not startLine or not endLine then
|
|
2673
|
+
return { error = "Instance path, startLine, and endLine are required" }
|
|
2674
|
+
end
|
|
2675
|
+
|
|
2676
|
+
local instance = getInstanceByPath(instancePath)
|
|
2677
|
+
if not instance then
|
|
2678
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2679
|
+
end
|
|
2680
|
+
|
|
2681
|
+
if not instance:IsA("LuaSourceContainer") then
|
|
2682
|
+
return { error = "Instance is not a script-like object: " .. instance.ClassName }
|
|
2683
|
+
end
|
|
2684
|
+
|
|
2685
|
+
local success, result = pcall(function()
|
|
2686
|
+
local lines, hadTrailingNewline = splitLines(instance.Source)
|
|
2687
|
+
local totalLines = #lines
|
|
2688
|
+
|
|
2689
|
+
if startLine < 1 or startLine > totalLines then
|
|
2690
|
+
error("startLine out of range (1-" .. totalLines .. ")")
|
|
2691
|
+
end
|
|
2692
|
+
if endLine < startLine or endLine > totalLines then
|
|
2693
|
+
error("endLine out of range (" .. startLine .. "-" .. totalLines .. ")")
|
|
2694
|
+
end
|
|
2695
|
+
|
|
2696
|
+
-- Build new source without the deleted lines
|
|
2697
|
+
local resultLines = {}
|
|
2698
|
+
|
|
2699
|
+
for i = 1, startLine - 1 do
|
|
2700
|
+
table.insert(resultLines, lines[i])
|
|
2701
|
+
end
|
|
2702
|
+
|
|
2703
|
+
for i = endLine + 1, totalLines do
|
|
2704
|
+
table.insert(resultLines, lines[i])
|
|
2705
|
+
end
|
|
2706
|
+
|
|
2707
|
+
local newSource = joinLines(resultLines, hadTrailingNewline)
|
|
2708
|
+
|
|
2709
|
+
-- Use UpdateSourceAsync for editor compatibility
|
|
2710
|
+
ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
|
|
2711
|
+
return newSource
|
|
2712
|
+
end)
|
|
2713
|
+
|
|
2714
|
+
ChangeHistoryService:SetWaypoint("Delete script lines " .. startLine .. "-" .. endLine .. ": " .. instance.Name)
|
|
2715
|
+
|
|
2716
|
+
return {
|
|
2717
|
+
success = true,
|
|
2718
|
+
instancePath = instancePath,
|
|
2719
|
+
deletedLines = { startLine = startLine, endLine = endLine },
|
|
2720
|
+
linesDeleted = endLine - startLine + 1,
|
|
2721
|
+
newLineCount = #resultLines,
|
|
2722
|
+
message = "Script lines deleted successfully"
|
|
2723
|
+
}
|
|
2724
|
+
end)
|
|
2725
|
+
|
|
2726
|
+
if success then
|
|
2727
|
+
return result
|
|
2728
|
+
else
|
|
2729
|
+
return { error = "Failed to delete script lines: " .. tostring(result) }
|
|
2730
|
+
end
|
|
2731
|
+
end
|
|
2732
|
+
|
|
2733
|
+
-- Attribute Tools: Get a single attribute
|
|
2734
|
+
handlers.getAttribute = function(requestData)
|
|
2735
|
+
local instancePath = requestData.instancePath
|
|
2736
|
+
local attributeName = requestData.attributeName
|
|
2737
|
+
|
|
2738
|
+
if not instancePath or not attributeName then
|
|
2739
|
+
return { error = "Instance path and attribute name are required" }
|
|
2740
|
+
end
|
|
2741
|
+
|
|
2742
|
+
local instance = getInstanceByPath(instancePath)
|
|
2743
|
+
if not instance then
|
|
2744
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2745
|
+
end
|
|
2746
|
+
|
|
2747
|
+
local success, result = pcall(function()
|
|
2748
|
+
local value = instance:GetAttribute(attributeName)
|
|
2749
|
+
local valueType = typeof(value)
|
|
2750
|
+
|
|
2751
|
+
-- Serialize the value for JSON transport
|
|
2752
|
+
local serializedValue = value
|
|
2753
|
+
if valueType == "Vector3" then
|
|
2754
|
+
serializedValue = { X = value.X, Y = value.Y, Z = value.Z, _type = "Vector3" }
|
|
2755
|
+
elseif valueType == "Color3" then
|
|
2756
|
+
serializedValue = { R = value.R, G = value.G, B = value.B, _type = "Color3" }
|
|
2757
|
+
elseif valueType == "CFrame" then
|
|
2758
|
+
serializedValue = { Position = { X = value.Position.X, Y = value.Position.Y, Z = value.Position.Z }, _type = "CFrame" }
|
|
2759
|
+
elseif valueType == "UDim2" then
|
|
2760
|
+
serializedValue = { X = { Scale = value.X.Scale, Offset = value.X.Offset }, Y = { Scale = value.Y.Scale, Offset = value.Y.Offset }, _type = "UDim2" }
|
|
2761
|
+
elseif valueType == "BrickColor" then
|
|
2762
|
+
serializedValue = { Name = value.Name, _type = "BrickColor" }
|
|
2763
|
+
end
|
|
2764
|
+
|
|
2765
|
+
return {
|
|
2766
|
+
instancePath = instancePath,
|
|
2767
|
+
attributeName = attributeName,
|
|
2768
|
+
value = serializedValue,
|
|
2769
|
+
valueType = valueType,
|
|
2770
|
+
exists = value ~= nil
|
|
2771
|
+
}
|
|
2772
|
+
end)
|
|
2773
|
+
|
|
2774
|
+
if success then
|
|
2775
|
+
return result
|
|
2776
|
+
else
|
|
2777
|
+
return { error = "Failed to get attribute: " .. tostring(result) }
|
|
2778
|
+
end
|
|
2779
|
+
end
|
|
2780
|
+
|
|
2781
|
+
-- Attribute Tools: Set an attribute
|
|
2782
|
+
handlers.setAttribute = function(requestData)
|
|
2783
|
+
local instancePath = requestData.instancePath
|
|
2784
|
+
local attributeName = requestData.attributeName
|
|
2785
|
+
local attributeValue = requestData.attributeValue
|
|
2786
|
+
local valueType = requestData.valueType -- Optional type hint
|
|
2787
|
+
|
|
2788
|
+
if not instancePath or not attributeName then
|
|
2789
|
+
return { error = "Instance path and attribute name are required" }
|
|
2790
|
+
end
|
|
2791
|
+
|
|
2792
|
+
local instance = getInstanceByPath(instancePath)
|
|
2793
|
+
if not instance then
|
|
2794
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2795
|
+
end
|
|
2796
|
+
|
|
2797
|
+
local success, result = pcall(function()
|
|
2798
|
+
local value = attributeValue
|
|
2799
|
+
|
|
2800
|
+
-- Handle special type conversions
|
|
2801
|
+
if type(attributeValue) == "table" then
|
|
2802
|
+
if attributeValue._type == "Vector3" or valueType == "Vector3" then
|
|
2803
|
+
value = Vector3.new(attributeValue.X or 0, attributeValue.Y or 0, attributeValue.Z or 0)
|
|
2804
|
+
elseif attributeValue._type == "Color3" or valueType == "Color3" then
|
|
2805
|
+
value = Color3.new(attributeValue.R or 0, attributeValue.G or 0, attributeValue.B or 0)
|
|
2806
|
+
elseif attributeValue._type == "UDim2" or valueType == "UDim2" then
|
|
2807
|
+
value = UDim2.new(
|
|
2808
|
+
attributeValue.X and attributeValue.X.Scale or 0,
|
|
2809
|
+
attributeValue.X and attributeValue.X.Offset or 0,
|
|
2810
|
+
attributeValue.Y and attributeValue.Y.Scale or 0,
|
|
2811
|
+
attributeValue.Y and attributeValue.Y.Offset or 0
|
|
2812
|
+
)
|
|
2813
|
+
elseif attributeValue._type == "BrickColor" or valueType == "BrickColor" then
|
|
2814
|
+
value = BrickColor.new(attributeValue.Name or "Medium stone grey")
|
|
2815
|
+
end
|
|
2816
|
+
end
|
|
2817
|
+
|
|
2818
|
+
instance:SetAttribute(attributeName, value)
|
|
2819
|
+
ChangeHistoryService:SetWaypoint("Set attribute " .. attributeName .. " on " .. instance.Name)
|
|
2820
|
+
|
|
2821
|
+
return {
|
|
2822
|
+
success = true,
|
|
2823
|
+
instancePath = instancePath,
|
|
2824
|
+
attributeName = attributeName,
|
|
2825
|
+
value = attributeValue,
|
|
2826
|
+
message = "Attribute set successfully"
|
|
2827
|
+
}
|
|
2828
|
+
end)
|
|
2829
|
+
|
|
2830
|
+
if success then
|
|
2831
|
+
return result
|
|
2832
|
+
else
|
|
2833
|
+
return { error = "Failed to set attribute: " .. tostring(result) }
|
|
2834
|
+
end
|
|
2835
|
+
end
|
|
2836
|
+
|
|
2837
|
+
-- Attribute Tools: Get all attributes
|
|
2838
|
+
handlers.getAttributes = function(requestData)
|
|
2839
|
+
local instancePath = requestData.instancePath
|
|
2840
|
+
|
|
2841
|
+
if not instancePath then
|
|
2842
|
+
return { error = "Instance path is required" }
|
|
2843
|
+
end
|
|
2844
|
+
|
|
2845
|
+
local instance = getInstanceByPath(instancePath)
|
|
2846
|
+
if not instance then
|
|
2847
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2848
|
+
end
|
|
2849
|
+
|
|
2850
|
+
local success, result = pcall(function()
|
|
2851
|
+
local attributes = instance:GetAttributes()
|
|
2852
|
+
local serializedAttributes = {}
|
|
2853
|
+
|
|
2854
|
+
for name, value in pairs(attributes) do
|
|
2855
|
+
local valueType = typeof(value)
|
|
2856
|
+
local serializedValue = value
|
|
2857
|
+
|
|
2858
|
+
if valueType == "Vector3" then
|
|
2859
|
+
serializedValue = { X = value.X, Y = value.Y, Z = value.Z, _type = "Vector3" }
|
|
2860
|
+
elseif valueType == "Color3" then
|
|
2861
|
+
serializedValue = { R = value.R, G = value.G, B = value.B, _type = "Color3" }
|
|
2862
|
+
elseif valueType == "CFrame" then
|
|
2863
|
+
serializedValue = { Position = { X = value.Position.X, Y = value.Position.Y, Z = value.Position.Z }, _type = "CFrame" }
|
|
2864
|
+
elseif valueType == "UDim2" then
|
|
2865
|
+
serializedValue = { X = { Scale = value.X.Scale, Offset = value.X.Offset }, Y = { Scale = value.Y.Scale, Offset = value.Y.Offset }, _type = "UDim2" }
|
|
2866
|
+
elseif valueType == "BrickColor" then
|
|
2867
|
+
serializedValue = { Name = value.Name, _type = "BrickColor" }
|
|
2868
|
+
end
|
|
2869
|
+
|
|
2870
|
+
serializedAttributes[name] = {
|
|
2871
|
+
value = serializedValue,
|
|
2872
|
+
type = valueType
|
|
2873
|
+
}
|
|
2874
|
+
end
|
|
2875
|
+
|
|
2876
|
+
local count = 0
|
|
2877
|
+
for _ in pairs(serializedAttributes) do count = count + 1 end
|
|
2878
|
+
|
|
2879
|
+
return {
|
|
2880
|
+
instancePath = instancePath,
|
|
2881
|
+
attributes = serializedAttributes,
|
|
2882
|
+
count = count
|
|
2883
|
+
}
|
|
2884
|
+
end)
|
|
2885
|
+
|
|
2886
|
+
if success then
|
|
2887
|
+
return result
|
|
2888
|
+
else
|
|
2889
|
+
return { error = "Failed to get attributes: " .. tostring(result) }
|
|
2890
|
+
end
|
|
2891
|
+
end
|
|
2892
|
+
|
|
2893
|
+
-- Attribute Tools: Delete an attribute
|
|
2894
|
+
handlers.deleteAttribute = function(requestData)
|
|
2895
|
+
local instancePath = requestData.instancePath
|
|
2896
|
+
local attributeName = requestData.attributeName
|
|
2897
|
+
|
|
2898
|
+
if not instancePath or not attributeName then
|
|
2899
|
+
return { error = "Instance path and attribute name are required" }
|
|
2900
|
+
end
|
|
2901
|
+
|
|
2902
|
+
local instance = getInstanceByPath(instancePath)
|
|
2903
|
+
if not instance then
|
|
2904
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2905
|
+
end
|
|
2906
|
+
|
|
2907
|
+
local success, result = pcall(function()
|
|
2908
|
+
local existed = instance:GetAttribute(attributeName) ~= nil
|
|
2909
|
+
instance:SetAttribute(attributeName, nil)
|
|
2910
|
+
ChangeHistoryService:SetWaypoint("Delete attribute " .. attributeName .. " from " .. instance.Name)
|
|
2911
|
+
|
|
2912
|
+
return {
|
|
2913
|
+
success = true,
|
|
2914
|
+
instancePath = instancePath,
|
|
2915
|
+
attributeName = attributeName,
|
|
2916
|
+
existed = existed,
|
|
2917
|
+
message = existed and "Attribute deleted successfully" or "Attribute did not exist"
|
|
2918
|
+
}
|
|
2919
|
+
end)
|
|
2920
|
+
|
|
2921
|
+
if success then
|
|
2922
|
+
return result
|
|
2923
|
+
else
|
|
2924
|
+
return { error = "Failed to delete attribute: " .. tostring(result) }
|
|
2925
|
+
end
|
|
2926
|
+
end
|
|
2927
|
+
|
|
2928
|
+
-- Tag Tools: Get all tags on an instance
|
|
2929
|
+
handlers.getTags = function(requestData)
|
|
2930
|
+
local instancePath = requestData.instancePath
|
|
2931
|
+
|
|
2932
|
+
if not instancePath then
|
|
2933
|
+
return { error = "Instance path is required" }
|
|
2934
|
+
end
|
|
2935
|
+
|
|
2936
|
+
local instance = getInstanceByPath(instancePath)
|
|
2937
|
+
if not instance then
|
|
2938
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2939
|
+
end
|
|
2940
|
+
|
|
2941
|
+
local success, result = pcall(function()
|
|
2942
|
+
local tags = CollectionService:GetTags(instance)
|
|
2943
|
+
|
|
2944
|
+
return {
|
|
2945
|
+
instancePath = instancePath,
|
|
2946
|
+
tags = tags,
|
|
2947
|
+
count = #tags
|
|
2948
|
+
}
|
|
2949
|
+
end)
|
|
2950
|
+
|
|
2951
|
+
if success then
|
|
2952
|
+
return result
|
|
2953
|
+
else
|
|
2954
|
+
return { error = "Failed to get tags: " .. tostring(result) }
|
|
2955
|
+
end
|
|
2956
|
+
end
|
|
2957
|
+
|
|
2958
|
+
-- Tag Tools: Add a tag to an instance
|
|
2959
|
+
handlers.addTag = function(requestData)
|
|
2960
|
+
local instancePath = requestData.instancePath
|
|
2961
|
+
local tagName = requestData.tagName
|
|
2962
|
+
|
|
2963
|
+
if not instancePath or not tagName then
|
|
2964
|
+
return { error = "Instance path and tag name are required" }
|
|
2965
|
+
end
|
|
2966
|
+
|
|
2967
|
+
local instance = getInstanceByPath(instancePath)
|
|
2968
|
+
if not instance then
|
|
2969
|
+
return { error = "Instance not found: " .. instancePath }
|
|
2970
|
+
end
|
|
2971
|
+
|
|
2972
|
+
local success, result = pcall(function()
|
|
2973
|
+
local alreadyHad = CollectionService:HasTag(instance, tagName)
|
|
2974
|
+
CollectionService:AddTag(instance, tagName)
|
|
2975
|
+
ChangeHistoryService:SetWaypoint("Add tag " .. tagName .. " to " .. instance.Name)
|
|
2976
|
+
|
|
2977
|
+
return {
|
|
2978
|
+
success = true,
|
|
2979
|
+
instancePath = instancePath,
|
|
2980
|
+
tagName = tagName,
|
|
2981
|
+
alreadyHad = alreadyHad,
|
|
2982
|
+
message = alreadyHad and "Instance already had this tag" or "Tag added successfully"
|
|
2983
|
+
}
|
|
2984
|
+
end)
|
|
2985
|
+
|
|
2986
|
+
if success then
|
|
2987
|
+
return result
|
|
2988
|
+
else
|
|
2989
|
+
return { error = "Failed to add tag: " .. tostring(result) }
|
|
2990
|
+
end
|
|
2991
|
+
end
|
|
2992
|
+
|
|
2993
|
+
-- Tag Tools: Remove a tag from an instance
|
|
2994
|
+
handlers.removeTag = function(requestData)
|
|
2995
|
+
local instancePath = requestData.instancePath
|
|
2996
|
+
local tagName = requestData.tagName
|
|
2997
|
+
|
|
2998
|
+
if not instancePath or not tagName then
|
|
2999
|
+
return { error = "Instance path and tag name are required" }
|
|
3000
|
+
end
|
|
3001
|
+
|
|
3002
|
+
local instance = getInstanceByPath(instancePath)
|
|
3003
|
+
if not instance then
|
|
3004
|
+
return { error = "Instance not found: " .. instancePath }
|
|
3005
|
+
end
|
|
3006
|
+
|
|
3007
|
+
local success, result = pcall(function()
|
|
3008
|
+
local hadTag = CollectionService:HasTag(instance, tagName)
|
|
3009
|
+
CollectionService:RemoveTag(instance, tagName)
|
|
3010
|
+
ChangeHistoryService:SetWaypoint("Remove tag " .. tagName .. " from " .. instance.Name)
|
|
3011
|
+
|
|
3012
|
+
return {
|
|
3013
|
+
success = true,
|
|
3014
|
+
instancePath = instancePath,
|
|
3015
|
+
tagName = tagName,
|
|
3016
|
+
hadTag = hadTag,
|
|
3017
|
+
message = hadTag and "Tag removed successfully" or "Instance did not have this tag"
|
|
3018
|
+
}
|
|
3019
|
+
end)
|
|
3020
|
+
|
|
3021
|
+
if success then
|
|
3022
|
+
return result
|
|
3023
|
+
else
|
|
3024
|
+
return { error = "Failed to remove tag: " .. tostring(result) }
|
|
3025
|
+
end
|
|
3026
|
+
end
|
|
3027
|
+
|
|
3028
|
+
-- Tag Tools: Get all instances with a specific tag
|
|
3029
|
+
handlers.getTagged = function(requestData)
|
|
3030
|
+
local tagName = requestData.tagName
|
|
3031
|
+
|
|
3032
|
+
if not tagName then
|
|
3033
|
+
return { error = "Tag name is required" }
|
|
3034
|
+
end
|
|
3035
|
+
|
|
3036
|
+
local success, result = pcall(function()
|
|
3037
|
+
local taggedInstances = CollectionService:GetTagged(tagName)
|
|
3038
|
+
local instances = {}
|
|
3039
|
+
|
|
3040
|
+
for _, instance in ipairs(taggedInstances) do
|
|
3041
|
+
table.insert(instances, {
|
|
3042
|
+
name = instance.Name,
|
|
3043
|
+
className = instance.ClassName,
|
|
3044
|
+
path = getInstancePath(instance)
|
|
3045
|
+
})
|
|
3046
|
+
end
|
|
3047
|
+
|
|
3048
|
+
return {
|
|
3049
|
+
tagName = tagName,
|
|
3050
|
+
instances = instances,
|
|
3051
|
+
count = #instances
|
|
3052
|
+
}
|
|
3053
|
+
end)
|
|
3054
|
+
|
|
3055
|
+
if success then
|
|
3056
|
+
return result
|
|
3057
|
+
else
|
|
3058
|
+
return { error = "Failed to get tagged instances: " .. tostring(result) }
|
|
3059
|
+
end
|
|
3060
|
+
end
|
|
3061
|
+
|
|
3062
|
+
-- Selection handlers
|
|
3063
|
+
handlers.getSelection = function(requestData)
|
|
3064
|
+
local selection = Selection:Get()
|
|
3065
|
+
|
|
3066
|
+
if #selection == 0 then
|
|
3067
|
+
return {
|
|
3068
|
+
success = true,
|
|
3069
|
+
selection = {},
|
|
3070
|
+
count = 0,
|
|
3071
|
+
message = "No objects selected"
|
|
3072
|
+
}
|
|
3073
|
+
end
|
|
3074
|
+
|
|
3075
|
+
local selectedObjects = {}
|
|
3076
|
+
for _, instance in ipairs(selection) do
|
|
3077
|
+
table.insert(selectedObjects, {
|
|
3078
|
+
name = instance.Name,
|
|
3079
|
+
className = instance.ClassName,
|
|
3080
|
+
path = getInstancePath(instance),
|
|
3081
|
+
parent = instance.Parent and getInstancePath(instance.Parent) or nil
|
|
3082
|
+
})
|
|
3083
|
+
end
|
|
3084
|
+
|
|
3085
|
+
return {
|
|
3086
|
+
success = true,
|
|
3087
|
+
selection = selectedObjects,
|
|
3088
|
+
count = #selection,
|
|
3089
|
+
message = #selection .. " object(s) selected"
|
|
3090
|
+
}
|
|
3091
|
+
end
|
|
3092
|
+
|
|
3093
|
+
local function updateUIState()
|
|
3094
|
+
if pluginState.isActive then
|
|
3095
|
+
statusLabel.Text = "Connecting..."
|
|
3096
|
+
statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
3097
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
3098
|
+
statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
3099
|
+
statusText.Text = "CONNECTING"
|
|
3100
|
+
if pluginState.consecutiveFailures == 0 then
|
|
3101
|
+
detailStatusLabel.Text = "HTTP: ... MCP: ..."
|
|
3102
|
+
else
|
|
3103
|
+
detailStatusLabel.Text = "HTTP: X MCP: X"
|
|
3104
|
+
end
|
|
3105
|
+
detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
|
|
3106
|
+
connectButton.Text = "Disconnect"
|
|
3107
|
+
connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
3108
|
+
startPulseAnimation()
|
|
3109
|
+
|
|
3110
|
+
-- Reset steps to connecting state
|
|
3111
|
+
step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
3112
|
+
step1Label.Text = "1. HTTP server reachable (connecting...)"
|
|
3113
|
+
step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
3114
|
+
step2Label.Text = "2. MCP bridge connected (connecting...)"
|
|
3115
|
+
step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
|
|
3116
|
+
step3Label.Text = "3. Ready for commands (connecting...)"
|
|
3117
|
+
pluginState.mcpWaitStartTime = nil
|
|
3118
|
+
troubleshootLabel.Visible = false
|
|
3119
|
+
|
|
3120
|
+
if not buttonHover then
|
|
3121
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
3122
|
+
end
|
|
3123
|
+
|
|
3124
|
+
|
|
3125
|
+
urlInput.TextEditable = false
|
|
3126
|
+
urlInput.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
|
|
3127
|
+
urlInput.BorderColor3 = Color3.fromRGB(75, 85, 99)
|
|
3128
|
+
else
|
|
3129
|
+
statusLabel.Text = "Disconnected"
|
|
3130
|
+
statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
3131
|
+
statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
3132
|
+
statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
3133
|
+
statusText.Text = "OFFLINE"
|
|
3134
|
+
detailStatusLabel.Text = "HTTP: X MCP: X"
|
|
3135
|
+
detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
3136
|
+
connectButton.Text = "Connect"
|
|
3137
|
+
connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
3138
|
+
stopPulseAnimation()
|
|
3139
|
+
|
|
3140
|
+
-- Reset steps to offline state
|
|
3141
|
+
step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
3142
|
+
step1Label.Text = "1. HTTP server reachable (offline)"
|
|
3143
|
+
step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
3144
|
+
step2Label.Text = "2. MCP bridge connected (offline)"
|
|
3145
|
+
step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
|
|
3146
|
+
step3Label.Text = "3. Ready for commands (offline)"
|
|
3147
|
+
pluginState.mcpWaitStartTime = nil
|
|
3148
|
+
troubleshootLabel.Visible = false
|
|
3149
|
+
|
|
3150
|
+
if not buttonHover then
|
|
3151
|
+
connectButton.BackgroundColor3 = Color3.fromRGB(16, 185, 129)
|
|
3152
|
+
end
|
|
3153
|
+
|
|
3154
|
+
|
|
3155
|
+
urlInput.TextEditable = true
|
|
3156
|
+
urlInput.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
|
|
3157
|
+
urlInput.BorderColor3 = Color3.fromRGB(99, 102, 241)
|
|
3158
|
+
end
|
|
3159
|
+
end
|
|
3160
|
+
|
|
3161
|
+
local function activatePlugin()
|
|
3162
|
+
pluginState.serverUrl = urlInput.Text
|
|
3163
|
+
|
|
3164
|
+
pluginState.isActive = true
|
|
3165
|
+
pluginState.consecutiveFailures = 0
|
|
3166
|
+
pluginState.currentRetryDelay = 0.5
|
|
3167
|
+
screenGui.Enabled = true
|
|
3168
|
+
updateUIState()
|
|
3169
|
+
|
|
3170
|
+
pcall(function()
|
|
3171
|
+
HttpService:RequestAsync({
|
|
3172
|
+
Url = pluginState.serverUrl .. "/ready",
|
|
3173
|
+
Method = "POST",
|
|
3174
|
+
Headers = {
|
|
3175
|
+
["Content-Type"] = "application/json",
|
|
3176
|
+
},
|
|
3177
|
+
Body = HttpService:JSONEncode({
|
|
3178
|
+
pluginReady = true,
|
|
3179
|
+
timestamp = tick(),
|
|
3180
|
+
}),
|
|
3181
|
+
})
|
|
3182
|
+
end)
|
|
3183
|
+
|
|
3184
|
+
if not pluginState.connection then
|
|
3185
|
+
pluginState.connection = RunService.Heartbeat:Connect(function()
|
|
3186
|
+
local now = tick()
|
|
3187
|
+
local currentInterval = pluginState.consecutiveFailures > 5 and pluginState.currentRetryDelay
|
|
3188
|
+
or pluginState.pollInterval
|
|
3189
|
+
if now - pluginState.lastPoll > currentInterval then
|
|
3190
|
+
pluginState.lastPoll = now
|
|
3191
|
+
pollForRequests()
|
|
3192
|
+
end
|
|
3193
|
+
end)
|
|
3194
|
+
end
|
|
3195
|
+
end
|
|
3196
|
+
|
|
3197
|
+
local function deactivatePlugin()
|
|
3198
|
+
pluginState.isActive = false
|
|
3199
|
+
updateUIState()
|
|
3200
|
+
|
|
3201
|
+
pcall(function()
|
|
3202
|
+
HttpService:RequestAsync({
|
|
3203
|
+
Url = pluginState.serverUrl .. "/disconnect",
|
|
3204
|
+
Method = "POST",
|
|
3205
|
+
Headers = {
|
|
3206
|
+
["Content-Type"] = "application/json",
|
|
3207
|
+
},
|
|
3208
|
+
Body = HttpService:JSONEncode({
|
|
3209
|
+
timestamp = tick(),
|
|
3210
|
+
}),
|
|
3211
|
+
})
|
|
3212
|
+
end)
|
|
3213
|
+
|
|
3214
|
+
if pluginState.connection then
|
|
3215
|
+
pluginState.connection:Disconnect()
|
|
3216
|
+
pluginState.connection = nil
|
|
3217
|
+
end
|
|
3218
|
+
|
|
3219
|
+
pluginState.consecutiveFailures = 0
|
|
3220
|
+
pluginState.currentRetryDelay = 0.5
|
|
3221
|
+
end
|
|
3222
|
+
|
|
3223
|
+
connectButton.Activated:Connect(function()
|
|
3224
|
+
if pluginState.isActive then
|
|
3225
|
+
deactivatePlugin()
|
|
3226
|
+
else
|
|
3227
|
+
activatePlugin()
|
|
3228
|
+
end
|
|
3229
|
+
end)
|
|
3230
|
+
|
|
3231
|
+
button.Click:Connect(function()
|
|
3232
|
+
screenGui.Enabled = not screenGui.Enabled
|
|
3233
|
+
end)
|
|
3234
|
+
|
|
3235
|
+
plugin.Unloading:Connect(function()
|
|
3236
|
+
deactivatePlugin()
|
|
3237
|
+
end)
|
|
3238
|
+
|
|
3239
|
+
updateUIState()
|
|
3240
|
+
]]></ProtectedString>
|
|
3241
|
+
<bool name="Disabled">false</bool>
|
|
3242
|
+
<Content name="LinkedSource"><null></null></Content>
|
|
3243
|
+
<token name="RunContext">0</token>
|
|
3244
|
+
<string name="ScriptGuid">{A3D0AD89-2E27-4747-90E9-3F5D15F7E0F9}</string>
|
|
3245
|
+
<BinaryString name="AttributesSerialize"></BinaryString>
|
|
3246
|
+
<SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
|
|
3247
|
+
<bool name="DefinesCapabilities">false</bool>
|
|
3248
|
+
<string name="Name">Script</string>
|
|
3249
|
+
<int64 name="SourceAssetId">-1</int64>
|
|
3250
|
+
<BinaryString name="Tags"></BinaryString>
|
|
3251
|
+
</Properties>
|
|
3252
|
+
</Item>
|
|
3253
|
+
</roblox>
|