@adiholick/nvstudio-mcp 1.0.4 → 2.0.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.
@@ -0,0 +1,1420 @@
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="Folder" referent="RBX0497167F99814CA6A2096BB729707905">
5
+ <Properties>
6
+ <BinaryString name="AttributesSerialize"></BinaryString>
7
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
8
+ <bool name="DefinesCapabilities">false</bool>
9
+ <string name="Name">nvstudio_mcp</string>
10
+ <int64 name="SourceAssetId">-1</int64>
11
+ <SharedString name="Tags">yuZpQdnvvUBOTYh1jqZ2cA==</SharedString>
12
+ </Properties>
13
+ <Item class="ModuleScript" referent="RBX_create_instance">
14
+ <Properties>
15
+ <BinaryString name="AttributesSerialize"></BinaryString>
16
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
17
+ <bool name="DefinesCapabilities">false</bool>
18
+ <bool name="Disabled">false</bool>
19
+ <Content name="LinkedSource"><null></null></Content>
20
+ <string name="Name">create_instance</string>
21
+ <int64 name="SourceAssetId">-1</int64>
22
+ <SharedString name="Tags"></SharedString>
23
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
24
+ local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
25
+ if not success or not config.ClassName then
26
+ return { status = "error", error = "Format data JSON tidak valid atau ClassName hilang." }
27
+ end
28
+
29
+ local newSuccess, newInst = pcall(function()
30
+ local inst = Instance.new(config.ClassName)
31
+ inst.Parent = targetInstance
32
+
33
+ if config.Properties then
34
+ for prop, val in pairs(config.Properties) do
35
+ pcall(function() inst[prop] = val end)
36
+ end
37
+ end
38
+ return inst
39
+ end)
40
+
41
+ if newSuccess then
42
+ return { status = "success", result = "Instance '"..config.ClassName.."' berhasil dibuat di " .. targetPath }
43
+ else
44
+ return { status = "error", error = "Gagal membuat Instance. Pastikan ClassName valid." }
45
+ end
46
+ end
47
+ ]]></ProtectedString>
48
+ </Properties>
49
+ </Item>
50
+ <Item class="ModuleScript" referent="RBX_delete_instance">
51
+ <Properties>
52
+ <BinaryString name="AttributesSerialize"></BinaryString>
53
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
54
+ <bool name="DefinesCapabilities">false</bool>
55
+ <bool name="Disabled">false</bool>
56
+ <Content name="LinkedSource"><null></null></Content>
57
+ <string name="Name">delete_instance</string>
58
+ <int64 name="SourceAssetId">-1</int64>
59
+ <SharedString name="Tags"></SharedString>
60
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
61
+ -- Guardrail: Cegah penghapusan Service utama
62
+ local protectedServices = {
63
+ "Workspace", "Players", "Lighting", "MaterialService", "NetworkClient",
64
+ "ReplicatedFirst", "ReplicatedStorage", "ServerScriptService", "ServerStorage",
65
+ "StarterGui", "StarterPack", "StarterPlayer", "Teams", "SoundService", "Chat",
66
+ "TextChatService", "VoiceChatService"
67
+ }
68
+
69
+ if targetInstance.Parent == game or targetInstance.Parent == nil then
70
+ for _, serviceName in ipairs(protectedServices) do
71
+ if targetInstance.Name == serviceName or targetInstance.ClassName == serviceName then
72
+ return { status = "error", error = "GUARDRAIL BLOCKED: Anda tidak diizinkan untuk menghapus layanan tingkat atas (" .. targetInstance.Name .. ")." }
73
+ end
74
+ end
75
+ end
76
+
77
+ local name = targetInstance.Name
78
+ local className = targetInstance.ClassName
79
+ local parentName = targetInstance.Parent and targetInstance.Parent.Name or "nil"
80
+
81
+ local success, err = pcall(function()
82
+ targetInstance:Destroy()
83
+ end)
84
+
85
+ if success then
86
+ return { status = "success", result = "Instance '" .. name .. "' (" .. className .. ") berhasil dihapus dari " .. parentName .. "." }
87
+ else
88
+ return { status = "error", error = "Gagal menghapus instance: " .. tostring(err) }
89
+ end
90
+ end
91
+ ]]></ProtectedString>
92
+ </Properties>
93
+ </Item>
94
+ <Item class="ModuleScript" referent="RBX_execute_luau">
95
+ <Properties>
96
+ <BinaryString name="AttributesSerialize"></BinaryString>
97
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
98
+ <bool name="DefinesCapabilities">false</bool>
99
+ <bool name="Disabled">false</bool>
100
+ <Content name="LinkedSource"><null></null></Content>
101
+ <string name="Name">execute_luau</string>
102
+ <int64 name="SourceAssetId">-1</int64>
103
+ <SharedString name="Tags"></SharedString>
104
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
105
+ local code = data
106
+ if not code or code == "" then
107
+ return { status = "error", error = "Kode yang dieksekusi tidak boleh kosong." }
108
+ end
109
+
110
+ local HttpService = game:GetService("HttpService")
111
+
112
+ -- Bungkus code ke dalam fungsi return
113
+ local wrappedCode = "return function()\n" .. code .. "\nend"
114
+
115
+ local moduleScript = Instance.new("ModuleScript")
116
+ moduleScript.Name = "AI_Execute_Temp"
117
+ moduleScript.Source = wrappedCode
118
+
119
+ -- Taruh di ReplicatedStorage sementara agar bisa di-require
120
+ moduleScript.Parent = game:GetService("ReplicatedStorage")
121
+
122
+ local resultData = nil
123
+ local execError = nil
124
+ local isFinished = false
125
+
126
+ -- Eksekusi asinkron dengan batas waktu (timeout) 5 detik
127
+ task.spawn(function()
128
+ local reqSuccess, func = pcall(function() return require(moduleScript) end)
129
+ if reqSuccess and type(func) == "function" then
130
+ local execSuccess, res = pcall(func)
131
+ if execSuccess then
132
+ resultData = res
133
+ else
134
+ execError = "Runtime Error: " .. tostring(res)
135
+ end
136
+ else
137
+ execError = "Syntax/Require Error: " .. tostring(func)
138
+ end
139
+ isFinished = true
140
+ end)
141
+
142
+ -- Tunggu hingga 5 detik
143
+ local timeWaited = 0
144
+ while not isFinished and timeWaited < 5 do
145
+ timeWaited = timeWaited + task.wait()
146
+ end
147
+
148
+ -- Pembersihan (Wajib dihancurkan)
149
+ pcall(function() moduleScript:Destroy() end)
150
+
151
+ if not isFinished then
152
+ return { status = "error", error = "Timeout 5 detik terlampaui. Kemungkinan ada infinite loop tanpa task.wait()." }
153
+ end
154
+
155
+ if execError then
156
+ return { status = "error", error = execError }
157
+ end
158
+
159
+ -- Cegah return nil error
160
+ if resultData == nil then
161
+ return { status = "success", result = "Eksekusi berhasil (Tidak ada nilai kembalian)." }
162
+ end
163
+
164
+ -- JSON Encode untuk mencegah raw Instance return crash
165
+ local serializeSuccess, jsonRes = pcall(function()
166
+ return HttpService:JSONEncode(resultData)
167
+ end)
168
+
169
+ if serializeSuccess then
170
+ return { status = "success", result = HttpService:JSONDecode(jsonRes) } -- decode kembali agar format aslinya diteruskan oleh router
171
+ else
172
+ return { status = "error", error = "Gagal menserialisasi hasil (Dilarang mengembalikan raw Roblox Instance): " .. tostring(jsonRes) }
173
+ end
174
+ end
175
+ ]]></ProtectedString>
176
+ </Properties>
177
+ </Item>
178
+ <Item class="ModuleScript" referent="RBX_generate_terrain">
179
+ <Properties>
180
+ <BinaryString name="AttributesSerialize"></BinaryString>
181
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
182
+ <bool name="DefinesCapabilities">false</bool>
183
+ <bool name="Disabled">false</bool>
184
+ <Content name="LinkedSource"><null></null></Content>
185
+ <string name="Name">generate_terrain</string>
186
+ <int64 name="SourceAssetId">-1</int64>
187
+ <SharedString name="Tags"></SharedString>
188
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
189
+ local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
190
+ if not success or not config.Size or not config.Position or not config.Material then
191
+ return { status = "error", error = "Format JSON tidak valid. Membutuhkan Size, Position, dan Material." }
192
+ end
193
+
194
+ local fillSuccess, err = pcall(function()
195
+ local terrain = game.Workspace.Terrain
196
+ local materialEnum = Enum.Material[config.Material] or Enum.Material.Grass
197
+
198
+ local size = Vector3.new(config.Size[1], config.Size[2], config.Size[3])
199
+ local position = Vector3.new(config.Position[1], config.Position[2], config.Position[3])
200
+
201
+ local region = Region3.new(position - (size/2), position + (size/2))
202
+ terrain:FillRegion(region:ExpandToGrid(4), 4, materialEnum)
203
+ end)
204
+
205
+ if fillSuccess then
206
+ return { status = "success", result = "Terrain (" .. config.Material .. ") berhasil di-generate." }
207
+ else
208
+ return { status = "error", error = "Gagal men-generate Terrain. Pastikan tipe data array angka valid: " .. tostring(err) }
209
+ end
210
+ end
211
+ ]]></ProtectedString>
212
+ </Properties>
213
+ </Item>
214
+ <Item class="ModuleScript" referent="RBX_get_children">
215
+ <Properties>
216
+ <BinaryString name="AttributesSerialize"></BinaryString>
217
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
218
+ <bool name="DefinesCapabilities">false</bool>
219
+ <bool name="Disabled">false</bool>
220
+ <Content name="LinkedSource"><null></null></Content>
221
+ <string name="Name">get_children</string>
222
+ <int64 name="SourceAssetId">-1</int64>
223
+ <SharedString name="Tags"></SharedString>
224
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
225
+ local children = {}
226
+ for _, child in ipairs(targetInstance:GetChildren()) do
227
+ table.insert(children, child.Name .. " (" .. child.ClassName .. ")")
228
+ end
229
+ return { status = "success", result = children }
230
+ end
231
+ ]]></ProtectedString>
232
+ </Properties>
233
+ </Item>
234
+ <Item class="ModuleScript" referent="RBX_get_logs">
235
+ <Properties>
236
+ <BinaryString name="AttributesSerialize"></BinaryString>
237
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
238
+ <bool name="DefinesCapabilities">false</bool>
239
+ <bool name="Disabled">false</bool>
240
+ <Content name="LinkedSource"><null></null></Content>
241
+ <string name="Name">get_logs</string>
242
+ <int64 name="SourceAssetId">-1</int64>
243
+ <SharedString name="Tags"></SharedString>
244
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
245
+ if #ctx.recentLogs == 0 then
246
+ return { status = "success", result = {"Console kosong. Tidak ada error atau log terbaru."} }
247
+ else
248
+ return { status = "success", result = ctx.recentLogs }
249
+ end
250
+ end
251
+ ]]></ProtectedString>
252
+ </Properties>
253
+ </Item>
254
+ <Item class="ModuleScript" referent="RBX_get_properties">
255
+ <Properties>
256
+ <BinaryString name="AttributesSerialize"></BinaryString>
257
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
258
+ <bool name="DefinesCapabilities">false</bool>
259
+ <bool name="Disabled">false</bool>
260
+ <Content name="LinkedSource"><null></null></Content>
261
+ <string name="Name">get_properties</string>
262
+ <int64 name="SourceAssetId">-1</int64>
263
+ <SharedString name="Tags"></SharedString>
264
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
265
+ local CollectionService = game:GetService("CollectionService")
266
+
267
+ local props = {
268
+ Name = targetInstance.Name,
269
+ ClassName = targetInstance.ClassName,
270
+ Parent = targetInstance.Parent and targetInstance.Parent.Name or "nil",
271
+ }
272
+
273
+ -- Ambil Properties Umum secara aman via pcall
274
+ local function tryProp(propName)
275
+ local success, val = pcall(function() return targetInstance[propName] end)
276
+ if success and val ~= nil then
277
+ if typeof(val) == "Vector3" then
278
+ props[propName] = {math.round(val.X*100)/100, math.round(val.Y*100)/100, math.round(val.Z*100)/100}
279
+ elseif typeof(val) == "Color3" then
280
+ props[propName] = {math.round(val.R * 255), math.round(val.G * 255), math.round(val.B * 255)}
281
+ elseif typeof(val) == "CFrame" then
282
+ props[propName] = tostring(val)
283
+ elseif typeof(val) == "EnumItem" then
284
+ props[propName] = val.Name
285
+ elseif typeof(val) == "UDim2" then
286
+ props[propName] = {val.X.Scale, val.X.Offset, val.Y.Scale, val.Y.Offset}
287
+ elseif typeof(val) == "Instance" then
288
+ props[propName] = val:GetFullName()
289
+ else
290
+ props[propName] = val
291
+ end
292
+ end
293
+ end
294
+
295
+ local commonProps = {
296
+ "Anchored", "CanCollide", "Transparency", "Reflectance",
297
+ "Position", "Size", "Color", "Material", "Text", "TextColor3",
298
+ "BackgroundTransparency", "BackgroundColor3", "Visible", "Enabled",
299
+ "Volume", "Playing", "Looped", "Value"
300
+ }
301
+
302
+ for _, prop in ipairs(commonProps) do
303
+ tryProp(prop)
304
+ end
305
+
306
+ -- Attributes & Tags
307
+ pcall(function()
308
+ props.Attributes = targetInstance:GetAttributes()
309
+ end)
310
+ pcall(function()
311
+ props.Tags = CollectionService:GetTags(targetInstance)
312
+ end)
313
+
314
+ return { status = "success", result = props }
315
+ end
316
+ ]]></ProtectedString>
317
+ </Properties>
318
+ </Item>
319
+ <Item class="ModuleScript" referent="RBX_get_script_source">
320
+ <Properties>
321
+ <BinaryString name="AttributesSerialize"></BinaryString>
322
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
323
+ <bool name="DefinesCapabilities">false</bool>
324
+ <bool name="Disabled">false</bool>
325
+ <Content name="LinkedSource"><null></null></Content>
326
+ <string name="Name">get_script_source</string>
327
+ <int64 name="SourceAssetId">-1</int64>
328
+ <SharedString name="Tags"></SharedString>
329
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
330
+ if targetInstance:IsA("LuaSourceContainer") then
331
+ return { status = "success", result = targetInstance.Source }
332
+ else
333
+ return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. "). Kamu WAJIB menggunakan get_children terlebih dahulu sebelum menarik source." }
334
+ end
335
+ end
336
+ ]]></ProtectedString>
337
+ </Properties>
338
+ </Item>
339
+ <Item class="ModuleScript" referent="RBX_insert_asset">
340
+ <Properties>
341
+ <BinaryString name="AttributesSerialize"></BinaryString>
342
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
343
+ <bool name="DefinesCapabilities">false</bool>
344
+ <bool name="Disabled">false</bool>
345
+ <Content name="LinkedSource"><null></null></Content>
346
+ <string name="Name">insert_asset</string>
347
+ <int64 name="SourceAssetId">-1</int64>
348
+ <SharedString name="Tags"></SharedString>
349
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
350
+ local InsertService = game:GetService("InsertService")
351
+ local assetId = tonumber(data)
352
+ if not assetId then
353
+ return { status = "error", error = "Asset ID harus berupa angka yang valid." }
354
+ end
355
+
356
+ local success, model = pcall(function()
357
+ return InsertService:LoadAsset(assetId)
358
+ end)
359
+
360
+ if success and model then
361
+ model.Parent = game.Workspace
362
+ return { status = "success", result = "Aset berhasil dimasukkan ke Workspace." }
363
+ else
364
+ return { status = "error", error = "Gagal memuat aset. Pastikan Asset ID benar dan akun/plugin Anda memiliki izin (Ownership/Public)." }
365
+ end
366
+ end
367
+ ]]></ProtectedString>
368
+ </Properties>
369
+ </Item>
370
+ <Item class="Script" referent="RBX_Main_server">
371
+ <Properties>
372
+ <BinaryString name="AttributesSerialize"></BinaryString>
373
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
374
+ <bool name="DefinesCapabilities">false</bool>
375
+ <bool name="Disabled">false</bool>
376
+ <Content name="LinkedSource"><null></null></Content>
377
+ <string name="Name">Main.server</string>
378
+ <int64 name="SourceAssetId">-1</int64>
379
+ <SharedString name="Tags"></SharedString>
380
+ <token name="RunContext">0</token>
381
+ <ProtectedString name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
382
+ local LogService = game:GetService("LogService")
383
+
384
+ local TASK_URL = "http://localhost:3055/api/tasks"
385
+ local RESPONSE_URL = "http://localhost:3055/api/response"
386
+
387
+ -- Generate Unique Studio ID (menghindari bentrok jika 2 Studio terbuka)
388
+ local STUDIO_ID = HttpService:GenerateGUID(false)
389
+
390
+ -- Import UI Manager (Procedural Dashboard)
391
+ local uiModule = script.Parent:FindFirstChild("ui_manager")
392
+ local ui = nil
393
+
394
+ -- Membangun Tombol Toolbar (Toggle)
395
+ local toolbar = plugin:CreateToolbar("NVStudio MCP")
396
+ local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722")
397
+
398
+ -- Global State & Context AI
399
+ local ctx = {
400
+ scriptHistory = {},
401
+ recentLogs = {},
402
+ HttpService = HttpService,
403
+ studioId = STUDIO_ID
404
+ }
405
+ local MAX_LOGS = 50
406
+ local isConnected = false
407
+ local autoConnect = plugin:GetSetting("AutoConnect") or false
408
+ local isPolling = false
409
+ local isClickDebounce = false -- Mencegah rapid double-click
410
+
411
+ -- Deklarasi fungsi polling di awal agar bisa dipanggil oleh UI
412
+ local startPolling
413
+ local stopPolling
414
+
415
+ -- Inisialisasi UI jika modul ditemukan
416
+ if uiModule then
417
+ local UIManager = require(uiModule)
418
+ ui = UIManager.init(plugin, STUDIO_ID)
419
+
420
+ -- Sync Initial State ke UI
421
+ ui:setAutoConnectState(autoConnect)
422
+ ui:setStatus("disconnected")
423
+
424
+ -- Event Toggle GUI Panel
425
+ toggleBtn.Click:Connect(function()
426
+ ui:toggleVisibility()
427
+ end)
428
+
429
+ -- Event Tombol Connect (dengan debounce untuk mencegah double-click)
430
+ ui.connectBtn.MouseButton1Click:Connect(function()
431
+ if isClickDebounce then return end
432
+ isClickDebounce = true
433
+
434
+ if not isConnected then
435
+ -- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
436
+ isManualDisconnect = false
437
+ isConnected = true
438
+ ui:setStatus("waiting")
439
+ ui:addLog("[System] Koneksi manual dimulai. Mencari AI Server (Waiting)...", Color3.fromRGB(255, 220, 100))
440
+ startPolling()
441
+ else
442
+ -- DISCONNECT — hentikan polling dan tandai manual disconnect
443
+ stopPolling()
444
+ ui:setStatus("disconnected")
445
+ ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
446
+ end
447
+
448
+ task.delay(0.3, function()
449
+ isClickDebounce = false
450
+ end)
451
+ end)
452
+
453
+ -- Event Custom Checkbox (Auto-Connect)
454
+ ui.autoConnectBtn.MouseButton1Click:Connect(function()
455
+ autoConnect = not autoConnect
456
+ plugin:SetSetting("AutoConnect", autoConnect)
457
+ ui:setAutoConnectState(autoConnect)
458
+ ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
459
+ end)
460
+ else
461
+ -- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
462
+ toggleBtn.Click:Connect(function()
463
+ print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan di dalam Folder. GUI Gagal dimuat!")
464
+ end)
465
+ end
466
+
467
+ -- Handler Output Studio (Untuk AI dan GUI)
468
+ local function addSystemLog(msg, msgType)
469
+ local color = Color3.fromRGB(200, 200, 200)
470
+ local prefix = "[INFO] "
471
+ if msgType == Enum.MessageType.MessageError then
472
+ prefix = "[ERROR] "
473
+ color = Color3.fromRGB(255, 100, 100)
474
+ elseif msgType == Enum.MessageType.MessageWarning then
475
+ prefix = "[WARN] "
476
+ color = Color3.fromRGB(255, 200, 100)
477
+ end
478
+
479
+ local fullMsg = prefix .. msg
480
+
481
+ -- 1. Simpan di memori untuk disedot AI
482
+ table.insert(ctx.recentLogs, fullMsg)
483
+ if #ctx.recentLogs > MAX_LOGS then
484
+ table.remove(ctx.recentLogs, 1)
485
+ end
486
+
487
+ -- 2. Cetak ke Terminal GUI Plugin
488
+ if ui then
489
+ ui:addLog(fullMsg, color)
490
+ end
491
+ end
492
+
493
+ -- Ikat listener output
494
+ LogService.MessageOut:Connect(addSystemLog)
495
+
496
+ -- Memuat semua module skills secara dinamis
497
+ local skills = {}
498
+ for _, module in ipairs(script.Parent:GetChildren()) do
499
+ if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
500
+ skills[module.Name] = require(module)
501
+ end
502
+ end
503
+
504
+ -- Fungsi Helper Pathing
505
+ local function getInstanceFromPath(pathString)
506
+ local parts = string.split(pathString, ".")
507
+ local current = game
508
+
509
+ if parts[1] == "game" then table.remove(parts, 1) end
510
+
511
+ -- Handle common Services directly to ensure reliability
512
+ pcall(function()
513
+ local svc = game:GetService(parts[1])
514
+ if svc then
515
+ current = svc
516
+ table.remove(parts, 1)
517
+ end
518
+ end)
519
+
520
+ for _, part in ipairs(parts) do
521
+ local found = current:FindFirstChild(part)
522
+ if not found then return nil end
523
+ current = found
524
+ end
525
+ return current
526
+ end
527
+
528
+ -- Memproses Task Eksekusi dari AI
529
+ local function processTask(taskData)
530
+ if not taskData or not taskData.id or not taskData.command then return nil end
531
+
532
+ if ui then
533
+ ui:addLog("[AI Command] Menerima instruksi: " .. taskData.command, Color3.fromRGB(150, 200, 255))
534
+ end
535
+
536
+ local targetInstance = getInstanceFromPath(taskData.target)
537
+ if not targetInstance then
538
+ return {
539
+ id = taskData.id,
540
+ status = "error",
541
+ error = "Target path tidak ditemukan: " .. taskData.target
542
+ }
543
+ end
544
+
545
+ local skillFunc = skills[taskData.command]
546
+ if not skillFunc then
547
+ return {
548
+ id = taskData.id,
549
+ status = "error",
550
+ error = "Command tidak dikenali oleh Studio: " .. taskData.command
551
+ }
552
+ end
553
+
554
+ -- Eksekusi skill AI
555
+ local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
556
+ result.id = taskData.id
557
+
558
+ -- Cetak hasil eksekusi ke Terminal GUI
559
+ if ui then
560
+ local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
561
+ ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
562
+ end
563
+
564
+ return result
565
+ end
566
+
567
+ -- Menghentikan polling dengan bersih (dipanggil oleh tombol Disconnect)
568
+ local isManualDisconnect = false -- Mencegah auto-reconnect saat user sengaja disconnect
569
+
570
+ function stopPolling()
571
+ isConnected = false
572
+ isManualDisconnect = true
573
+ -- isPolling akan menjadi false secara otomatis saat loop keluar
574
+ end
575
+
576
+ -- Background Polling Loop (Dapat dihentikan oleh user)
577
+ function startPolling()
578
+ if isPolling then return end
579
+ isPolling = true
580
+
581
+ task.spawn(function()
582
+ while isConnected do
583
+ local success, response = pcall(function()
584
+ -- Tambahkan cache-buster agar Studio tidak me-cache response HTTP
585
+ local cacheBuster = "?t=" .. tostring(os.clock())
586
+ return HttpService:GetAsync(TASK_URL .. cacheBuster)
587
+ end)
588
+
589
+ -- PENTING: Cek ulang isConnected SETELAH HTTP request selesai.
590
+ if not isConnected then
591
+ break
592
+ end
593
+
594
+ if success and response then
595
+ -- JIKA PING SUKSES, Node.js menyala (IDE / Agen Sedang Terbuka!)
596
+ if ui then ui:setStatus("connected") end
597
+
598
+ local decodeSuccess, taskData = pcall(function()
599
+ return HttpService:JSONDecode(response)
600
+ end)
601
+
602
+ if decodeSuccess and taskData and taskData.id then
603
+ local resultData = processTask(taskData)
604
+ if resultData then
605
+ pcall(function()
606
+ local jsonResult = HttpService:JSONEncode(resultData)
607
+ HttpService:PostAsync(RESPONSE_URL, jsonResult, Enum.HttpContentType.ApplicationJson)
608
+ end)
609
+ end
610
+ end
611
+
612
+ -- Fast-polling (long-polling yield di server Node.js)
613
+ -- 50ms untuk safety mencegah Studio Engine freeze jika Node.js membalas instant
614
+ task.wait(0.05)
615
+ else
616
+ -- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
617
+ if ui then ui:setStatus("waiting") end
618
+ task.wait(1)
619
+ end
620
+
621
+ -- Cek lagi sebelum sleep (agar disconnect lebih responsif)
622
+ if not isConnected then
623
+ break
624
+ end
625
+ end
626
+ isPolling = false
627
+ end)
628
+ end
629
+
630
+ -- ══════════════════════════════════════════════════════════════════════
631
+ -- AUTO-DETECT: Background heartbeat yang SELALU berjalan.
632
+ -- Mendeteksi apakah server MCP aktif setiap 2 detik.
633
+ -- Jika terdeteksi aktif → otomatis connect (tanpa perlu tekan tombol).
634
+ -- Jika terdeteksi mati → otomatis update status.
635
+ -- ══════════════════════════════════════════════════════════════════════
636
+ task.spawn(function()
637
+ task.wait(1) -- Tunggu sebentar agar UI siap
638
+
639
+ if ui then
640
+ ui:addLog("[System] Plugin termuat. Auto-detect server MCP aktif...", Color3.fromRGB(200, 200, 200))
641
+ end
642
+
643
+ while true do
644
+ -- Skip heartbeat jika polling loop sudah aktif (ia sudah menangani semuanya)
645
+ if not isPolling then
646
+ local success, errorMsg = pcall(function()
647
+ local cacheBuster = "?t=" .. tostring(os.clock())
648
+ return HttpService:GetAsync(TASK_URL .. cacheBuster)
649
+ end)
650
+
651
+ if success then
652
+ -- Server terdeteksi aktif!
653
+ if not isConnected and not isManualDisconnect then
654
+ isConnected = true
655
+ if ui then
656
+ ui:setStatus("connected")
657
+ ui:addLog("[System] Server MCP terdeteksi! Auto-connected.", Color3.fromRGB(100, 255, 100))
658
+ end
659
+ startPolling()
660
+ end
661
+ else
662
+ -- Server tidak aktif
663
+ if isConnected then
664
+ -- Server baru saja mati (sebelumnya connected) — polling loop juga akan exit
665
+ isConnected = false
666
+ if ui then
667
+ ui:setStatus("waiting")
668
+ ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
669
+ end
670
+ elseif not isManualDisconnect then
671
+ if ui then
672
+ ui:setStatus("waiting")
673
+ -- HANYA print error jika belum terkoneksi, untuk debugging
674
+ ui:addLog("[Debug] Deteksi gagal: " .. tostring(errorMsg), Color3.fromRGB(150, 150, 150))
675
+ end
676
+ end
677
+ end
678
+ end
679
+
680
+ task.wait(2)
681
+ end
682
+ end)
683
+ ]]></ProtectedString>
684
+ </Properties>
685
+ </Item>
686
+ <Item class="ModuleScript" referent="RBX_patch_script_source">
687
+ <Properties>
688
+ <BinaryString name="AttributesSerialize"></BinaryString>
689
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
690
+ <bool name="DefinesCapabilities">false</bool>
691
+ <bool name="Disabled">false</bool>
692
+ <Content name="LinkedSource"><null></null></Content>
693
+ <string name="Name">patch_script_source</string>
694
+ <int64 name="SourceAssetId">-1</int64>
695
+ <SharedString name="Tags"></SharedString>
696
+ <ProtectedString name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
697
+
698
+ return function(targetInstance, data, ctx, targetPath)
699
+ if not targetInstance:IsA("LuaSourceContainer") then
700
+ return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
701
+ end
702
+
703
+ local payload = nil
704
+ pcall(function()
705
+ payload = HttpService:JSONDecode(data)
706
+ end)
707
+
708
+ if not payload then
709
+ return { status = "error", error = "Payload data harus berupa JSON valid." }
710
+ end
711
+
712
+ local currentSource = targetInstance.Source
713
+ local newSource = currentSource
714
+ local affectedLines = 0
715
+
716
+ -- Mode 1: Search & Replace Exact Chunk
717
+ if payload.target_content and payload.replacement_content then
718
+ local targetContent = payload.target_content
719
+ local replacementContent = payload.replacement_content
720
+ local allowMultiple = payload.allow_multiple or false
721
+
722
+ -- Escape magic characters for string.gsub if needed, or use string.find manually.
723
+ -- string.gsub doesn't do plain text replacement directly without escaping.
724
+ -- We'll write a plain text replace function.
725
+
726
+ local function plainReplace(str, pattern, repl, max)
727
+ local res = ""
728
+ local startIdx = 1
729
+ local count = 0
730
+ while true do
731
+ if max and count >= max then break end
732
+ local findStart, findEnd = string.find(str, pattern, startIdx, true) -- true for plain match
733
+ if not findStart then break end
734
+
735
+ res = res .. string.sub(str, startIdx, findStart - 1) .. repl
736
+ startIdx = findEnd + 1
737
+ count = count + 1
738
+ end
739
+ res = res .. string.sub(str, startIdx)
740
+ return res, count
741
+ end
742
+
743
+ local _, occurrences = plainReplace(currentSource, targetContent, replacementContent)
744
+
745
+ if occurrences == 0 then
746
+ return { status = "error", error = "target_content tidak ditemukan di dalam script." }
747
+ elseif occurrences > 1 and not allowMultiple then
748
+ return { status = "error", error = "target_content ditemukan " .. occurrences .. " kali. Tolong sertakan baris konteks lebih banyak agar unik, atau set allow_multiple = true." }
749
+ end
750
+
751
+ newSource, affectedLines = plainReplace(currentSource, targetContent, replacementContent)
752
+ affectedLines = affectedLines -- not exact line count, but occurrences count
753
+
754
+ -- Mode 2: Line Range Replace
755
+ elseif payload.start_line and payload.end_line and payload.replacement_content then
756
+ local lines = string.split(currentSource, "\n")
757
+ local startLine = tonumber(payload.start_line)
758
+ local endLine = tonumber(payload.end_line)
759
+
760
+ if not startLine or not endLine or startLine < 1 or endLine > #lines or startLine > endLine then
761
+ return { status = "error", error = "Range baris tidak valid. Jumlah baris script: " .. #lines }
762
+ end
763
+
764
+ local newLines = {}
765
+ for i = 1, startLine - 1 do
766
+ table.insert(newLines, lines[i])
767
+ end
768
+
769
+ table.insert(newLines, payload.replacement_content)
770
+
771
+ for i = endLine + 1, #lines do
772
+ table.insert(newLines, lines[i])
773
+ end
774
+
775
+ newSource = table.concat(newLines, "\n")
776
+ affectedLines = (endLine - startLine) + 1
777
+ else
778
+ return { status = "error", error = "Payload tidak lengkap. Butuh (target_content & replacement_content) atau (start_line, end_line & replacement_content)." }
779
+ end
780
+
781
+ -- Simpan backup snapshot sebelum mengaplikasikan patch
782
+ ctx.scriptHistory[targetPath] = currentSource
783
+
784
+ -- Terapkan perubahan
785
+ targetInstance.Source = newSource
786
+
787
+ return {
788
+ status = "success",
789
+ result = "Patch berhasil diterapkan pada '" .. targetInstance.Name .. "'. Perubahan diaplikasikan.",
790
+ affected_lines = affectedLines
791
+ }
792
+ end
793
+ ]]></ProtectedString>
794
+ </Properties>
795
+ </Item>
796
+ <Item class="ModuleScript" referent="RBX_rollback_script">
797
+ <Properties>
798
+ <BinaryString name="AttributesSerialize"></BinaryString>
799
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
800
+ <bool name="DefinesCapabilities">false</bool>
801
+ <bool name="Disabled">false</bool>
802
+ <Content name="LinkedSource"><null></null></Content>
803
+ <string name="Name">rollback_script</string>
804
+ <int64 name="SourceAssetId">-1</int64>
805
+ <SharedString name="Tags"></SharedString>
806
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
807
+ if targetInstance:IsA("LuaSourceContainer") then
808
+ local prevSource = ctx.scriptHistory[targetPath]
809
+ if prevSource then
810
+ targetInstance.Source = prevSource
811
+ return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback ke versi sebelumnya." }
812
+ else
813
+ return { status = "error", error = "Tidak ada riwayat backup sebelumnya di sesi Studio ini untuk: " .. targetPath }
814
+ end
815
+ else
816
+ return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
817
+ end
818
+ end
819
+ ]]></ProtectedString>
820
+ </Properties>
821
+ </Item>
822
+ <Item class="ModuleScript" referent="RBX_script_grep">
823
+ <Properties>
824
+ <BinaryString name="AttributesSerialize"></BinaryString>
825
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
826
+ <bool name="DefinesCapabilities">false</bool>
827
+ <bool name="Disabled">false</bool>
828
+ <Content name="LinkedSource"><null></null></Content>
829
+ <string name="Name">script_grep</string>
830
+ <int64 name="SourceAssetId">-1</int64>
831
+ <SharedString name="Tags"></SharedString>
832
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
833
+ local query = tostring(data)
834
+ if not query or query == "" then
835
+ return { status = "error", error = "Query pencarian tidak boleh kosong." }
836
+ end
837
+
838
+ local matches = {}
839
+ local matchCount = 0
840
+ local MAX_MATCHES = 50
841
+
842
+ local searchableServices = {
843
+ game:GetService("Workspace"),
844
+ game:GetService("ServerScriptService"),
845
+ game:GetService("ReplicatedStorage"),
846
+ game:GetService("StarterPlayer"),
847
+ game:GetService("StarterGui")
848
+ }
849
+
850
+ local function searchInScript(scriptObj)
851
+ local success, source = pcall(function() return scriptObj.Source end)
852
+ if not success or not source then return end
853
+
854
+ local lines = string.split(source, "\n")
855
+ for i, line in ipairs(lines) do
856
+ -- Coba cari dengan plain text dulu, jika gagal coba pattern
857
+ local found = string.find(line, query, 1, true)
858
+ if not found then
859
+ pcall(function() found = string.find(line, query) end)
860
+ end
861
+
862
+ if found then
863
+ table.insert(matches, {
864
+ script = scriptObj:GetFullName(),
865
+ line = i,
866
+ content = string.match(line, "^%s*(.-)%s*$") or line
867
+ })
868
+ matchCount = matchCount + 1
869
+ if matchCount >= MAX_MATCHES then
870
+ return true
871
+ end
872
+ end
873
+ end
874
+ return false
875
+ end
876
+
877
+ for _, service in ipairs(searchableServices) do
878
+ pcall(function()
879
+ local descendants = service:GetDescendants()
880
+ for _, desc in ipairs(descendants) do
881
+ if desc:IsA("LuaSourceContainer") then
882
+ if searchInScript(desc) then
883
+ break
884
+ end
885
+ end
886
+ end
887
+ end)
888
+ if matchCount >= MAX_MATCHES then
889
+ break
890
+ end
891
+ end
892
+
893
+ return { status = "success", matches = matches, count = matchCount, limitReached = (matchCount >= MAX_MATCHES) }
894
+ end
895
+ ]]></ProtectedString>
896
+ </Properties>
897
+ </Item>
898
+ <Item class="ModuleScript" referent="RBX_search_instance">
899
+ <Properties>
900
+ <BinaryString name="AttributesSerialize"></BinaryString>
901
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
902
+ <bool name="DefinesCapabilities">false</bool>
903
+ <bool name="Disabled">false</bool>
904
+ <Content name="LinkedSource"><null></null></Content>
905
+ <string name="Name">search_instance</string>
906
+ <int64 name="SourceAssetId">-1</int64>
907
+ <SharedString name="Tags"></SharedString>
908
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
909
+ local searchName = tostring(data)
910
+ local results = {}
911
+
912
+ local searchableServices = {game:GetService("Workspace"), game:GetService("ReplicatedStorage"), game:GetService("ServerScriptService"), game:GetService("StarterGui")}
913
+
914
+ for _, service in ipairs(searchableServices) do
915
+ for _, desc in ipairs(service:GetDescendants()) do
916
+ if desc.Name == searchName then
917
+ table.insert(results, desc:GetFullName())
918
+ end
919
+ end
920
+ end
921
+
922
+ if #results > 0 then
923
+ return { status = "success", result = results }
924
+ else
925
+ return { status = "success", result = "Tidak ditemukan instance dengan nama: " .. searchName }
926
+ end
927
+ end
928
+ ]]></ProtectedString>
929
+ </Properties>
930
+ </Item>
931
+ <Item class="ModuleScript" referent="RBX_ui_manager">
932
+ <Properties>
933
+ <BinaryString name="AttributesSerialize"></BinaryString>
934
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
935
+ <bool name="DefinesCapabilities">false</bool>
936
+ <bool name="Disabled">false</bool>
937
+ <Content name="LinkedSource"><null></null></Content>
938
+ <string name="Name">ui_manager</string>
939
+ <int64 name="SourceAssetId">-1</int64>
940
+ <SharedString name="Tags"></SharedString>
941
+ <ProtectedString name="Source"><![CDATA[local UIManager = {}
942
+ local widget = nil
943
+
944
+ function UIManager.init(pluginInstance, studioId)
945
+ -- Membuat Widget Panel yang bisa di-dock (menempel)
946
+ local widgetInfo = DockWidgetPluginGuiInfo.new(
947
+ Enum.InitialDockState.Right,
948
+ false, -- Initially hidden until toolbar button is clicked
949
+ false, -- Override enabled
950
+ 380, -- Default width
951
+ 520, -- Default height
952
+ 280, -- Min width
953
+ 320 -- Min height
954
+ )
955
+
956
+ widget = pluginInstance:CreateDockWidgetPluginGui("NVStudioMCP_GUI_V3", widgetInfo)
957
+ widget.Title = "NVStudio MCP Dashboard"
958
+
959
+ -- ═══════════════════════════════════════════
960
+ -- ROOT FRAME (Non-scrolling, fills entire widget)
961
+ -- ═══════════════════════════════════════════
962
+ local rootFrame = Instance.new("Frame")
963
+ rootFrame.Size = UDim2.new(1, 0, 1, 0)
964
+ rootFrame.BackgroundColor3 = Color3.fromRGB(22, 22, 26)
965
+ rootFrame.BorderSizePixel = 0
966
+ rootFrame.Parent = widget
967
+
968
+ local rootPadding = Instance.new("UIPadding")
969
+ rootPadding.PaddingTop = UDim.new(0, 8)
970
+ rootPadding.PaddingLeft = UDim.new(0, 10)
971
+ rootPadding.PaddingRight = UDim.new(0, 10)
972
+ rootPadding.PaddingBottom = UDim.new(0, 8)
973
+ rootPadding.Parent = rootFrame
974
+
975
+ local rootLayout = Instance.new("UIListLayout")
976
+ rootLayout.SortOrder = Enum.SortOrder.LayoutOrder
977
+ rootLayout.Padding = UDim.new(0, 6)
978
+ rootLayout.Parent = rootFrame
979
+
980
+ -- ═══════════════════════════════════════════
981
+ -- SECTION 1: HEADER CARD
982
+ -- ═══════════════════════════════════════════
983
+ local headerFrame = Instance.new("Frame")
984
+ headerFrame.AutomaticSize = Enum.AutomaticSize.Y
985
+ headerFrame.Size = UDim2.new(1, 0, 0, 0)
986
+ headerFrame.BackgroundColor3 = Color3.fromRGB(32, 32, 38)
987
+ headerFrame.BorderSizePixel = 0
988
+ headerFrame.LayoutOrder = 1
989
+ headerFrame.Parent = rootFrame
990
+
991
+ local hCorner = Instance.new("UICorner")
992
+ hCorner.CornerRadius = UDim.new(0, 6)
993
+ hCorner.Parent = headerFrame
994
+
995
+ local hPadding = Instance.new("UIPadding")
996
+ hPadding.PaddingTop = UDim.new(0, 10)
997
+ hPadding.PaddingLeft = UDim.new(0, 12)
998
+ hPadding.PaddingRight = UDim.new(0, 12)
999
+ hPadding.PaddingBottom = UDim.new(0, 10)
1000
+ hPadding.Parent = headerFrame
1001
+
1002
+ local hLayout = Instance.new("UIListLayout")
1003
+ hLayout.SortOrder = Enum.SortOrder.LayoutOrder
1004
+ hLayout.Padding = UDim.new(0, 2)
1005
+ hLayout.Parent = headerFrame
1006
+
1007
+ -- Title Row (Name + Status side by side)
1008
+ local titleRow = Instance.new("Frame")
1009
+ titleRow.AutomaticSize = Enum.AutomaticSize.Y
1010
+ titleRow.Size = UDim2.new(1, 0, 0, 0)
1011
+ titleRow.BackgroundTransparency = 1
1012
+ titleRow.LayoutOrder = 1
1013
+ titleRow.Parent = headerFrame
1014
+
1015
+ local titleLabel = Instance.new("TextLabel")
1016
+ titleLabel.Text = "NVStudio MCP"
1017
+ titleLabel.Font = Enum.Font.GothamBold
1018
+ titleLabel.TextSize = 18
1019
+ titleLabel.TextColor3 = Color3.fromRGB(240, 240, 245)
1020
+ titleLabel.BackgroundTransparency = 1
1021
+ titleLabel.Size = UDim2.new(0.6, 0, 0, 22)
1022
+ titleLabel.TextXAlignment = Enum.TextXAlignment.Left
1023
+ titleLabel.Parent = titleRow
1024
+
1025
+ local statusLabel = Instance.new("TextLabel")
1026
+ statusLabel.Text = "● Disconnected"
1027
+ statusLabel.Font = Enum.Font.GothamBold
1028
+ statusLabel.TextSize = 12
1029
+ statusLabel.TextColor3 = Color3.fromRGB(255, 100, 100)
1030
+ statusLabel.BackgroundTransparency = 1
1031
+ statusLabel.Size = UDim2.new(0.4, 0, 0, 22)
1032
+ statusLabel.Position = UDim2.new(0.6, 0, 0, 0)
1033
+ statusLabel.TextXAlignment = Enum.TextXAlignment.Right
1034
+ statusLabel.Parent = titleRow
1035
+
1036
+ -- Version
1037
+ local versionLabel = Instance.new("TextLabel")
1038
+ versionLabel.Text = "v1.2.0 · Hybrid Agent"
1039
+ versionLabel.Font = Enum.Font.Gotham
1040
+ versionLabel.TextSize = 11
1041
+ versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)
1042
+ versionLabel.BackgroundTransparency = 1
1043
+ versionLabel.Size = UDim2.new(1, 0, 0, 14)
1044
+ versionLabel.TextXAlignment = Enum.TextXAlignment.Left
1045
+ versionLabel.LayoutOrder = 2
1046
+ versionLabel.Parent = headerFrame
1047
+
1048
+ -- Thin separator
1049
+ local sep1 = Instance.new("Frame")
1050
+ sep1.Size = UDim2.new(1, 0, 0, 1)
1051
+ sep1.BackgroundColor3 = Color3.fromRGB(50, 50, 58)
1052
+ sep1.BorderSizePixel = 0
1053
+ sep1.LayoutOrder = 3
1054
+ sep1.Parent = headerFrame
1055
+
1056
+ -- Place info
1057
+ local placeLabel = Instance.new("TextLabel")
1058
+ placeLabel.Text = "📁 " .. (game.Name ~= "" and game.Name or "Untitled Baseplate")
1059
+ placeLabel.Font = Enum.Font.Gotham
1060
+ placeLabel.TextSize = 11
1061
+ placeLabel.TextColor3 = Color3.fromRGB(180, 180, 190)
1062
+ placeLabel.BackgroundTransparency = 1
1063
+ placeLabel.Size = UDim2.new(1, 0, 0, 16)
1064
+ placeLabel.TextXAlignment = Enum.TextXAlignment.Left
1065
+ placeLabel.LayoutOrder = 4
1066
+ placeLabel.Parent = headerFrame
1067
+
1068
+ -- Studio ID
1069
+ local idLabel = Instance.new("TextLabel")
1070
+ idLabel.Text = "🔗 " .. string.sub(tostring(studioId), 1, 18) .. "..."
1071
+ idLabel.Font = Enum.Font.Code
1072
+ idLabel.TextSize = 10
1073
+ idLabel.TextColor3 = Color3.fromRGB(90, 180, 230)
1074
+ idLabel.BackgroundTransparency = 1
1075
+ idLabel.Size = UDim2.new(1, 0, 0, 14)
1076
+ idLabel.TextXAlignment = Enum.TextXAlignment.Left
1077
+ idLabel.LayoutOrder = 5
1078
+ idLabel.Parent = headerFrame
1079
+
1080
+ -- Server URL
1081
+ local serverLabel = Instance.new("TextLabel")
1082
+ serverLabel.Text = "🌐 localhost:3055"
1083
+ serverLabel.Font = Enum.Font.Code
1084
+ serverLabel.TextSize = 10
1085
+ serverLabel.TextColor3 = Color3.fromRGB(140, 140, 150)
1086
+ serverLabel.BackgroundTransparency = 1
1087
+ serverLabel.Size = UDim2.new(1, 0, 0, 14)
1088
+ serverLabel.TextXAlignment = Enum.TextXAlignment.Left
1089
+ serverLabel.LayoutOrder = 6
1090
+ serverLabel.Parent = headerFrame
1091
+
1092
+ -- ═══════════════════════════════════════════
1093
+ -- SECTION 2: CONTROLS ROW
1094
+ -- ═══════════════════════════════════════════
1095
+ local controlsFrame = Instance.new("Frame")
1096
+ controlsFrame.Size = UDim2.new(1, 0, 0, 32)
1097
+ controlsFrame.BackgroundTransparency = 1
1098
+ controlsFrame.LayoutOrder = 2
1099
+ controlsFrame.Parent = rootFrame
1100
+
1101
+ -- Connect / Disconnect Button
1102
+ local connectBtn = Instance.new("TextButton")
1103
+ connectBtn.Size = UDim2.new(0.52, -4, 1, 0)
1104
+ connectBtn.Position = UDim2.new(0, 0, 0, 0)
1105
+ connectBtn.Text = " Connect"
1106
+ connectBtn.Font = Enum.Font.GothamBold
1107
+ connectBtn.TextSize = 13
1108
+ connectBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
1109
+ connectBtn.BackgroundColor3 = Color3.fromRGB(40, 160, 80)
1110
+ connectBtn.BorderSizePixel = 0
1111
+ connectBtn.AutoButtonColor = false
1112
+ connectBtn.ZIndex = 2
1113
+ connectBtn.Parent = controlsFrame
1114
+
1115
+ local cCorner = Instance.new("UICorner")
1116
+ cCorner.CornerRadius = UDim.new(0, 5)
1117
+ cCorner.Parent = connectBtn
1118
+
1119
+ -- Hover Effects for Connect Button (menggunakan stored base color, bukan text check)
1120
+ local _btnBaseColor = Color3.fromRGB(40, 160, 80)
1121
+ local _isHovering = false
1122
+
1123
+ connectBtn.MouseEnter:Connect(function()
1124
+ _isHovering = true
1125
+ connectBtn.BackgroundColor3 = Color3.fromRGB(
1126
+ math.min(255, math.floor(_btnBaseColor.R * 255 + 25)),
1127
+ math.min(255, math.floor(_btnBaseColor.G * 255 + 25)),
1128
+ math.min(255, math.floor(_btnBaseColor.B * 255 + 25))
1129
+ )
1130
+ end)
1131
+ connectBtn.MouseLeave:Connect(function()
1132
+ _isHovering = false
1133
+ connectBtn.BackgroundColor3 = _btnBaseColor
1134
+ end)
1135
+
1136
+ -- Auto-Connect Checkbox Container
1137
+ local autoConnectContainer = Instance.new("Frame")
1138
+ autoConnectContainer.Size = UDim2.new(0.48, -4, 1, 0)
1139
+ autoConnectContainer.Position = UDim2.new(0.52, 4, 0, 0)
1140
+ autoConnectContainer.BackgroundColor3 = Color3.fromRGB(32, 32, 38)
1141
+ autoConnectContainer.BorderSizePixel = 0
1142
+ autoConnectContainer.Parent = controlsFrame
1143
+
1144
+ local acCorner = Instance.new("UICorner")
1145
+ acCorner.CornerRadius = UDim.new(0, 5)
1146
+ acCorner.Parent = autoConnectContainer
1147
+
1148
+ -- Checkbox visual (outer box)
1149
+ local checkboxOuter = Instance.new("Frame")
1150
+ checkboxOuter.Size = UDim2.new(0, 16, 0, 16)
1151
+ checkboxOuter.Position = UDim2.new(0, 10, 0.5, -8)
1152
+ checkboxOuter.BackgroundColor3 = Color3.fromRGB(55, 55, 65)
1153
+ checkboxOuter.BorderSizePixel = 0
1154
+ checkboxOuter.Parent = autoConnectContainer
1155
+
1156
+ local cbCorner = Instance.new("UICorner")
1157
+ cbCorner.CornerRadius = UDim.new(0, 3)
1158
+ cbCorner.Parent = checkboxOuter
1159
+
1160
+ local cbStroke = Instance.new("UIStroke")
1161
+ cbStroke.Color = Color3.fromRGB(80, 80, 95)
1162
+ cbStroke.Thickness = 1
1163
+ cbStroke.Parent = checkboxOuter
1164
+
1165
+ -- Checkbox inner (check mark)
1166
+ local checkboxInner = Instance.new("Frame")
1167
+ checkboxInner.Size = UDim2.new(0, 10, 0, 10)
1168
+ checkboxInner.Position = UDim2.new(0.5, -5, 0.5, -5)
1169
+ checkboxInner.BackgroundColor3 = Color3.fromRGB(80, 170, 255)
1170
+ checkboxInner.Visible = false
1171
+ checkboxInner.Parent = checkboxOuter
1172
+
1173
+ local cbiCorner = Instance.new("UICorner")
1174
+ cbiCorner.CornerRadius = UDim.new(0, 2)
1175
+ cbiCorner.Parent = checkboxInner
1176
+
1177
+ -- Label
1178
+ local autoConnectLabel = Instance.new("TextLabel")
1179
+ autoConnectLabel.Text = "Auto-Connect"
1180
+ autoConnectLabel.Font = Enum.Font.Gotham
1181
+ autoConnectLabel.TextSize = 11
1182
+ autoConnectLabel.TextColor3 = Color3.fromRGB(180, 180, 190)
1183
+ autoConnectLabel.BackgroundTransparency = 1
1184
+ autoConnectLabel.Position = UDim2.new(0, 32, 0, 0)
1185
+ autoConnectLabel.Size = UDim2.new(1, -36, 1, 0)
1186
+ autoConnectLabel.TextXAlignment = Enum.TextXAlignment.Left
1187
+ autoConnectLabel.Parent = autoConnectContainer
1188
+
1189
+ -- Invisible overlay button for checkbox hit area
1190
+ local autoConnectBtn = Instance.new("TextButton")
1191
+ autoConnectBtn.Size = UDim2.new(1, 0, 1, 0)
1192
+ autoConnectBtn.BackgroundTransparency = 1
1193
+ autoConnectBtn.Text = ""
1194
+ autoConnectBtn.ZIndex = 3 -- Di atas elemen visual agar klik selalu terdeteksi
1195
+ autoConnectBtn.Parent = autoConnectContainer
1196
+
1197
+ -- Hover effect for checkbox container
1198
+ autoConnectBtn.MouseEnter:Connect(function()
1199
+ autoConnectContainer.BackgroundColor3 = Color3.fromRGB(42, 42, 50)
1200
+ end)
1201
+ autoConnectBtn.MouseLeave:Connect(function()
1202
+ autoConnectContainer.BackgroundColor3 = Color3.fromRGB(32, 32, 38)
1203
+ end)
1204
+
1205
+ -- ═══════════════════════════════════════════
1206
+ -- SECTION 3: TERMINAL LOG (fills remaining space)
1207
+ -- ═══════════════════════════════════════════
1208
+ -- Terminal Label
1209
+ local termLabel = Instance.new("TextLabel")
1210
+ termLabel.Text = "TERMINAL"
1211
+ termLabel.Font = Enum.Font.GothamBold
1212
+ termLabel.TextSize = 10
1213
+ termLabel.TextColor3 = Color3.fromRGB(90, 90, 100)
1214
+ termLabel.BackgroundTransparency = 1
1215
+ termLabel.Size = UDim2.new(1, 0, 0, 14)
1216
+ termLabel.TextXAlignment = Enum.TextXAlignment.Left
1217
+ termLabel.LayoutOrder = 3
1218
+ termLabel.Parent = rootFrame
1219
+
1220
+ -- Terminal container: Fills ALL remaining vertical space
1221
+ local terminalContainer = Instance.new("Frame")
1222
+ terminalContainer.BackgroundColor3 = Color3.fromRGB(14, 14, 18)
1223
+ terminalContainer.BorderSizePixel = 0
1224
+ terminalContainer.LayoutOrder = 4
1225
+ terminalContainer.ClipsDescendants = true
1226
+ terminalContainer.Parent = rootFrame
1227
+
1228
+ local termCorner = Instance.new("UICorner")
1229
+ termCorner.CornerRadius = UDim.new(0, 5)
1230
+ termCorner.Parent = terminalContainer
1231
+
1232
+ local termStroke = Instance.new("UIStroke")
1233
+ termStroke.Color = Color3.fromRGB(40, 40, 48)
1234
+ termStroke.Thickness = 1
1235
+ termStroke.Parent = terminalContainer
1236
+
1237
+ -- Scrolling log area inside terminal
1238
+ local scrollingFrame = Instance.new("ScrollingFrame")
1239
+ scrollingFrame.Size = UDim2.new(1, -8, 1, -4)
1240
+ scrollingFrame.Position = UDim2.new(0, 4, 0, 2)
1241
+ scrollingFrame.BackgroundTransparency = 1
1242
+ scrollingFrame.BorderSizePixel = 0
1243
+ scrollingFrame.ScrollBarThickness = 4
1244
+ scrollingFrame.ScrollBarImageColor3 = Color3.fromRGB(70, 70, 85)
1245
+ scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
1246
+ scrollingFrame.Parent = terminalContainer
1247
+
1248
+ local listLayout = Instance.new("UIListLayout")
1249
+ listLayout.Padding = UDim.new(0, 1)
1250
+ listLayout.SortOrder = Enum.SortOrder.LayoutOrder
1251
+ listLayout.Parent = scrollingFrame
1252
+
1253
+ -- ═══════════════════════════════════════════
1254
+ -- RESPONSIVITY: Dynamically size terminal to fill remaining space
1255
+ -- ═══════════════════════════════════════════
1256
+ local function updateLayout()
1257
+ -- Calculate how much space header + controls + termLabel use
1258
+ local usedHeight = rootLayout.AbsoluteContentSize.Y - terminalContainer.AbsoluteSize.Y
1259
+ local availableHeight = rootFrame.AbsoluteSize.Y - 16 -- rootPadding top+bottom
1260
+ local termHeight = math.max(100, availableHeight - usedHeight)
1261
+ terminalContainer.Size = UDim2.new(1, 0, 0, termHeight)
1262
+ end
1263
+
1264
+ -- Fire on widget resize
1265
+ widget:GetPropertyChangedSignal("AbsoluteSize"):Connect(updateLayout)
1266
+ -- Also fire once after initial render
1267
+ task.defer(updateLayout)
1268
+
1269
+ -- ═══════════════════════════════════════════
1270
+ -- Store references for external access
1271
+ -- ═══════════════════════════════════════════
1272
+ UIManager.widget = widget
1273
+ UIManager.connectBtn = connectBtn
1274
+ UIManager.autoConnectBtn = autoConnectBtn
1275
+ UIManager.checkboxInner = checkboxInner
1276
+ UIManager.checkboxStroke = cbStroke
1277
+ UIManager.statusLabel = statusLabel
1278
+ UIManager.scrollingFrame = scrollingFrame
1279
+ UIManager.listLayout = listLayout
1280
+ UIManager.terminalContainer = terminalContainer
1281
+ UIManager.termStroke = termStroke
1282
+ UIManager.logsCount = 0
1283
+
1284
+ -- Simpan referensi hover state agar setStatus bisa update base color
1285
+ UIManager._btnBaseColor = _btnBaseColor
1286
+ UIManager._isHovering = _isHovering
1287
+ UIManager._updateBtnBaseColor = function(newColor)
1288
+ _btnBaseColor = newColor
1289
+ if not _isHovering then
1290
+ connectBtn.BackgroundColor3 = newColor
1291
+ end
1292
+ end
1293
+
1294
+ return UIManager
1295
+ end
1296
+
1297
+ -- ═══════════════════════════════════════════
1298
+ -- Menambahkan baris log baru ke terminal GUI
1299
+ -- ═══════════════════════════════════════════
1300
+ function UIManager:addLog(text, color)
1301
+ local timestamp = os.date("%H:%M:%S")
1302
+ local displayText = "[" .. timestamp .. "] " .. text
1303
+
1304
+ local logLabel = Instance.new("TextLabel")
1305
+ logLabel.Text = displayText
1306
+ logLabel.Font = Enum.Font.Code
1307
+ logLabel.TextSize = 11
1308
+ logLabel.TextColor3 = color or Color3.fromRGB(180, 180, 190)
1309
+ logLabel.BackgroundTransparency = 1
1310
+ logLabel.TextWrapped = true
1311
+ logLabel.TextXAlignment = Enum.TextXAlignment.Left
1312
+ logLabel.RichText = false
1313
+ logLabel.AutomaticSize = Enum.AutomaticSize.Y
1314
+ logLabel.Size = UDim2.new(1, -6, 0, 0) -- Width fill, height auto
1315
+
1316
+ local logPadding = Instance.new("UIPadding")
1317
+ logPadding.PaddingLeft = UDim.new(0, 4)
1318
+ logPadding.PaddingTop = UDim.new(0, 1)
1319
+ logPadding.PaddingBottom = UDim.new(0, 1)
1320
+ logPadding.Parent = logLabel
1321
+
1322
+ logLabel.LayoutOrder = self.logsCount
1323
+ logLabel.Parent = self.scrollingFrame
1324
+ self.logsCount = self.logsCount + 1
1325
+
1326
+ -- Limit max logs to prevent memory bloat
1327
+ if self.logsCount > 150 then
1328
+ local first = self.scrollingFrame:FindFirstChildWhichIsA("TextLabel")
1329
+ if first then first:Destroy() end
1330
+ end
1331
+
1332
+ -- Update canvas size after a frame so AutomaticSize has resolved
1333
+ task.defer(function()
1334
+ self.scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, self.listLayout.AbsoluteContentSize.Y + 4)
1335
+ self.scrollingFrame.CanvasPosition = Vector2.new(0, math.max(0, self.listLayout.AbsoluteContentSize.Y - self.scrollingFrame.AbsoluteSize.Y))
1336
+ end)
1337
+ end
1338
+
1339
+ -- ═══════════════════════════════════════════
1340
+ -- 3 State Setter (Disconnected, Waiting, Connected)
1341
+ -- ═══════════════════════════════════════════
1342
+ function UIManager:setStatus(stateStr)
1343
+ if stateStr == "disconnected" then
1344
+ self.connectBtn.Text = " Connect"
1345
+ self._updateBtnBaseColor(Color3.fromRGB(40, 160, 80))
1346
+ self.statusLabel.Text = "● Disconnected"
1347
+ self.statusLabel.TextColor3 = Color3.fromRGB(255, 100, 100)
1348
+ self.termStroke.Color = Color3.fromRGB(40, 40, 48)
1349
+
1350
+ elseif stateStr == "waiting" then
1351
+ self.connectBtn.Text = " Disconnect"
1352
+ self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
1353
+ self.statusLabel.Text = "● Waiting for AI..."
1354
+ self.statusLabel.TextColor3 = Color3.fromRGB(255, 200, 80)
1355
+ self.termStroke.Color = Color3.fromRGB(130, 110, 50)
1356
+
1357
+ elseif stateStr == "connected" then
1358
+ self.connectBtn.Text = " Disconnect"
1359
+ self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
1360
+ self.statusLabel.Text = "● Connected"
1361
+ self.statusLabel.TextColor3 = Color3.fromRGB(100, 230, 120)
1362
+ self.termStroke.Color = Color3.fromRGB(50, 120, 60)
1363
+ end
1364
+ end
1365
+
1366
+ -- Mengubah visual custom checkbox
1367
+ function UIManager:setAutoConnectState(isAuto)
1368
+ self.checkboxInner.Visible = isAuto
1369
+ if isAuto then
1370
+ self.checkboxStroke.Color = Color3.fromRGB(80, 170, 255)
1371
+ else
1372
+ self.checkboxStroke.Color = Color3.fromRGB(80, 80, 95)
1373
+ end
1374
+ end
1375
+
1376
+ -- Sakelar visibilitas panel GUI
1377
+ function UIManager:toggleVisibility()
1378
+ if self.widget then
1379
+ self.widget.Enabled = not self.widget.Enabled
1380
+ end
1381
+ end
1382
+
1383
+ return UIManager
1384
+ ]]></ProtectedString>
1385
+ </Properties>
1386
+ </Item>
1387
+ <Item class="ModuleScript" referent="RBX_update_script_source">
1388
+ <Properties>
1389
+ <BinaryString name="AttributesSerialize"></BinaryString>
1390
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1391
+ <bool name="DefinesCapabilities">false</bool>
1392
+ <bool name="Disabled">false</bool>
1393
+ <Content name="LinkedSource"><null></null></Content>
1394
+ <string name="Name">update_script_source</string>
1395
+ <int64 name="SourceAssetId">-1</int64>
1396
+ <SharedString name="Tags"></SharedString>
1397
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
1398
+ if targetInstance:IsA("LuaSourceContainer") then
1399
+ local newCode = data or ""
1400
+
1401
+ -- Catatan: Validasi AST menggunakan loadstring() sengaja DIHAPUS.
1402
+ -- Karena secara default Roblox Studio mematikan LoadStringEnabled, menggunakannya
1403
+ -- hanya akan membuat AI gagal terus menerus untuk menulis script.
1404
+ -- Error syntax akan ditangkap secara native oleh Roblox Output.
1405
+
1406
+ ctx.scriptHistory[targetPath] = targetInstance.Source
1407
+ targetInstance.Source = newCode
1408
+ return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil diperbarui." }
1409
+ else
1410
+ return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
1411
+ end
1412
+ end
1413
+ ]]></ProtectedString>
1414
+ </Properties>
1415
+ </Item>
1416
+ </Item>
1417
+ <SharedStrings>
1418
+ <SharedString md5="yuZpQdnvvUBOTYh1jqZ2cA=="></SharedString>
1419
+ </SharedStrings>
1420
+ </roblox>