@adiholick/nvstudio-mcp 2.1.8 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -8
- package/dist/index.js +31 -15
- package/dist/luau-validator.js +26 -57
- package/dist/roblox-bridge.js +89 -56
- package/dist/task-queue.js +15 -1
- package/package.json +1 -1
- package/studio-plugin/Main.server.lua +50 -22
- package/studio-plugin/create_instance.lua +20 -10
- package/studio-plugin/delete_instance.lua +9 -0
- package/studio-plugin/execute_luau.lua +25 -6
- package/studio-plugin/generate_terrain.lua +2 -1
- package/studio-plugin/get_children.lua +7 -1
- package/studio-plugin/get_full_tree.lua +46 -0
- package/studio-plugin/insert_asset.lua +8 -3
- package/studio-plugin/nvstudio_mcp.rbxmx +356 -69
- package/studio-plugin/patch_script_source.lua +24 -4
- package/studio-plugin/rollback_script.lua +9 -3
- package/studio-plugin/script_grep.lua +24 -9
- package/studio-plugin/search_instance.lua +24 -5
- package/studio-plugin/set_properties.lua +73 -0
- package/studio-plugin/ui_manager.lua +4 -4
- package/studio-plugin/update_script_source.lua +5 -1
|
@@ -29,20 +29,30 @@
|
|
|
29
29
|
local newSuccess, newInst = pcall(function()
|
|
30
30
|
local inst = Instance.new(config.ClassName)
|
|
31
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
32
|
return inst
|
|
39
33
|
end)
|
|
40
34
|
|
|
41
|
-
if newSuccess then
|
|
42
|
-
return { status = "
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
if not newSuccess then
|
|
36
|
+
return { status = "error", error = "Gagal membuat Instance: " .. tostring(newInst) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
local propResultMsg = ""
|
|
40
|
+
if config.Properties and next(config.Properties) then
|
|
41
|
+
local setPropsFunc = require(script.Parent:FindFirstChild("set_properties"))
|
|
42
|
+
if setPropsFunc then
|
|
43
|
+
local propData = ctx.HttpService:JSONEncode(config.Properties)
|
|
44
|
+
local propRes = setPropsFunc(newInst, propData, ctx, "")
|
|
45
|
+
if propRes.status == "error" then
|
|
46
|
+
propResultMsg = " (Namun ada error properti: " .. propRes.result .. ")"
|
|
47
|
+
elseif propRes.result then
|
|
48
|
+
propResultMsg = " (" .. propRes.result .. ")"
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
propResultMsg = " (Gagal meload set_properties)"
|
|
52
|
+
end
|
|
45
53
|
end
|
|
54
|
+
|
|
55
|
+
return { status = "success", result = "Instance '"..config.ClassName.."' berhasil dibuat di " .. targetPath .. propResultMsg }
|
|
46
56
|
end
|
|
47
57
|
]]></ProtectedString>
|
|
48
58
|
</Properties>
|
|
@@ -58,6 +68,10 @@ end
|
|
|
58
68
|
<int64 name="SourceAssetId">-1</int64>
|
|
59
69
|
<BinaryString name="Tags"></BinaryString>
|
|
60
70
|
<ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
|
|
71
|
+
if targetInstance == game or targetInstance.Parent == game then
|
|
72
|
+
return { status = "error", error = "Tidak dapat menghapus root DataModel atau Service utama!" }
|
|
73
|
+
end
|
|
74
|
+
|
|
61
75
|
-- Guardrail: Cegah penghapusan Service utama
|
|
62
76
|
local protectedServices = {
|
|
63
77
|
"Workspace", "Players", "Lighting", "MaterialService", "NetworkClient",
|
|
@@ -78,6 +92,11 @@ end
|
|
|
78
92
|
local className = targetInstance.ClassName
|
|
79
93
|
local parentName = targetInstance.Parent and targetInstance.Parent.Name or "nil"
|
|
80
94
|
|
|
95
|
+
local childrenCount = #targetInstance:GetDescendants()
|
|
96
|
+
if childrenCount > 10 and data ~= "force" then
|
|
97
|
+
return { status = "error", error = "GUARDRAIL: Instance '" .. name .. "' memiliki " .. childrenCount .. " descendants. Jika Anda yakin ingin menghapus seluruhnya beserta isinya, sertakan data='force'." }
|
|
98
|
+
end
|
|
99
|
+
|
|
81
100
|
local success, err = pcall(function()
|
|
82
101
|
targetInstance:Destroy()
|
|
83
102
|
end)
|
|
@@ -113,7 +132,7 @@ end
|
|
|
113
132
|
local wrappedCode = "return function()\n" .. code .. "\nend"
|
|
114
133
|
|
|
115
134
|
local moduleScript = Instance.new("ModuleScript")
|
|
116
|
-
moduleScript.Name = "
|
|
135
|
+
moduleScript.Name = "AI_Execute_" .. HttpService:GenerateGUID(false):gsub("-", "")
|
|
117
136
|
moduleScript.Source = wrappedCode
|
|
118
137
|
|
|
119
138
|
-- Taruh di ReplicatedStorage sementara agar bisa di-require
|
|
@@ -124,10 +143,17 @@ end
|
|
|
124
143
|
local isFinished = false
|
|
125
144
|
|
|
126
145
|
-- Eksekusi asinkron dengan batas waktu (timeout) 5 detik
|
|
127
|
-
|
|
146
|
+
local logs = {}
|
|
147
|
+
|
|
148
|
+
local execThread = task.spawn(function()
|
|
128
149
|
local reqSuccess, func = pcall(function() return require(moduleScript) end)
|
|
129
150
|
if reqSuccess and type(func) == "function" then
|
|
151
|
+
local LogService = game:GetService("LogService")
|
|
152
|
+
local connection = LogService.MessageOut:Connect(function(msg, msgType)
|
|
153
|
+
table.insert(logs, msg)
|
|
154
|
+
end)
|
|
130
155
|
local execSuccess, res = pcall(func)
|
|
156
|
+
pcall(function() connection:Disconnect() end)
|
|
131
157
|
if execSuccess then
|
|
132
158
|
resultData = res
|
|
133
159
|
else
|
|
@@ -145,20 +171,31 @@ end
|
|
|
145
171
|
timeWaited = timeWaited + task.wait()
|
|
146
172
|
end
|
|
147
173
|
|
|
174
|
+
|
|
175
|
+
if not isFinished then
|
|
176
|
+
pcall(function() task.cancel(execThread) end)
|
|
177
|
+
end
|
|
178
|
+
|
|
148
179
|
-- Pembersihan (Wajib dihancurkan)
|
|
149
180
|
pcall(function() moduleScript:Destroy() end)
|
|
150
181
|
|
|
151
182
|
if not isFinished then
|
|
152
|
-
return { status = "error", error = "Timeout 5 detik terlampaui.
|
|
183
|
+
return { status = "error", error = "Timeout 5 detik terlampaui. Thread mungkin tetap berjalan di background jika terjebak dalam infinite loop tanpa task.wait(). PASTIKAN menambahkan task.wait() di dalam loop komputasi berat." }
|
|
153
184
|
end
|
|
154
185
|
|
|
155
186
|
if execError then
|
|
156
|
-
return { status = "error", error = execError }
|
|
187
|
+
return { status = "error", error = execError, logs = #logs > 0 and logs or nil }
|
|
157
188
|
end
|
|
158
189
|
|
|
190
|
+
local returnPayload = { status = "success" }
|
|
191
|
+
if #logs > 0 then
|
|
192
|
+
returnPayload.logs = logs
|
|
193
|
+
end
|
|
194
|
+
|
|
159
195
|
-- Cegah return nil error
|
|
160
196
|
if resultData == nil then
|
|
161
|
-
|
|
197
|
+
returnPayload.result = "Eksekusi berhasil (Tidak ada nilai kembalian)."
|
|
198
|
+
return returnPayload
|
|
162
199
|
end
|
|
163
200
|
|
|
164
201
|
-- JSON Encode untuk mencegah raw Instance return crash
|
|
@@ -167,7 +204,8 @@ end
|
|
|
167
204
|
end)
|
|
168
205
|
|
|
169
206
|
if serializeSuccess then
|
|
170
|
-
|
|
207
|
+
returnPayload.result = HttpService:JSONDecode(jsonRes) -- decode kembali agar format aslinya diteruskan oleh router
|
|
208
|
+
return returnPayload
|
|
171
209
|
else
|
|
172
210
|
return { status = "error", error = "Gagal menserialisasi hasil (Dilarang mengembalikan raw Roblox Instance): " .. tostring(jsonRes) }
|
|
173
211
|
end
|
|
@@ -193,7 +231,8 @@ end
|
|
|
193
231
|
|
|
194
232
|
local fillSuccess, err = pcall(function()
|
|
195
233
|
local terrain = game.Workspace.Terrain
|
|
196
|
-
local materialEnum = Enum.Material[config.Material]
|
|
234
|
+
local matOk, materialEnum = pcall(function() return Enum.Material[config.Material] end)
|
|
235
|
+
if not matOk then materialEnum = Enum.Material.Grass end
|
|
197
236
|
|
|
198
237
|
local size = Vector3.new(config.Size[1], config.Size[2], config.Size[3])
|
|
199
238
|
local position = Vector3.new(config.Position[1], config.Position[2], config.Position[3])
|
|
@@ -223,11 +262,76 @@ end
|
|
|
223
262
|
<BinaryString name="Tags"></BinaryString>
|
|
224
263
|
<ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
|
|
225
264
|
local children = {}
|
|
226
|
-
|
|
265
|
+
local allChildren = targetInstance:GetChildren()
|
|
266
|
+
local MAX_LIMIT = 200
|
|
267
|
+
for i, child in ipairs(allChildren) do
|
|
268
|
+
if i > MAX_LIMIT then
|
|
269
|
+
table.insert(children, "... (Terpotong, " .. tostring(#allChildren - MAX_LIMIT) .. " children lainnya tidak ditampilkan)")
|
|
270
|
+
break
|
|
271
|
+
end
|
|
227
272
|
table.insert(children, child.Name .. " (" .. child.ClassName .. ")")
|
|
228
273
|
end
|
|
229
274
|
return { status = "success", result = children }
|
|
230
275
|
end
|
|
276
|
+
]]></ProtectedString>
|
|
277
|
+
</Properties>
|
|
278
|
+
</Item>
|
|
279
|
+
<Item class="ModuleScript" referent="RBX_get_full_tree">
|
|
280
|
+
<Properties>
|
|
281
|
+
<BinaryString name="AttributesSerialize"></BinaryString>
|
|
282
|
+
<SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
|
|
283
|
+
<bool name="DefinesCapabilities">false</bool>
|
|
284
|
+
<bool name="Disabled">false</bool>
|
|
285
|
+
<Content name="LinkedSource"><null></null></Content>
|
|
286
|
+
<string name="Name">get_full_tree</string>
|
|
287
|
+
<int64 name="SourceAssetId">-1</int64>
|
|
288
|
+
<BinaryString name="Tags"></BinaryString>
|
|
289
|
+
<ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
|
|
290
|
+
local maxDepth = 2
|
|
291
|
+
if data and data ~= "" then
|
|
292
|
+
pcall(function() maxDepth = tonumber(data) or 2 end)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
local totalNodes = 0
|
|
296
|
+
local HARD_LIMIT = 2000 -- safeguard
|
|
297
|
+
|
|
298
|
+
local hardLimitReached = false
|
|
299
|
+
|
|
300
|
+
local function traverse(inst, currentDepth)
|
|
301
|
+
if hardLimitReached then return nil end
|
|
302
|
+
totalNodes = totalNodes + 1
|
|
303
|
+
local node = { name = inst.Name, className = inst.ClassName }
|
|
304
|
+
|
|
305
|
+
if totalNodes >= HARD_LIMIT then
|
|
306
|
+
hardLimitReached = true
|
|
307
|
+
return { name = "... (terpotong, melebihi limit " .. HARD_LIMIT .. " nodes)", className = "Warning" }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
if currentDepth >= maxDepth then
|
|
311
|
+
local childCount = #inst:GetChildren()
|
|
312
|
+
if childCount > 0 then
|
|
313
|
+
node.children = "... (" .. childCount .. " children tersembunyi)"
|
|
314
|
+
end
|
|
315
|
+
return node
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
local children = inst:GetChildren()
|
|
319
|
+
if #children > 0 then
|
|
320
|
+
node.children = {}
|
|
321
|
+
for _, child in ipairs(children) do
|
|
322
|
+
local childNode = traverse(child, currentDepth + 1)
|
|
323
|
+
if childNode then
|
|
324
|
+
table.insert(node.children, childNode)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
return node
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
local tree = traverse(targetInstance, 0)
|
|
333
|
+
return { status = "success", result = tree }
|
|
334
|
+
end
|
|
231
335
|
]]></ProtectedString>
|
|
232
336
|
</Properties>
|
|
233
337
|
</Item>
|
|
@@ -348,7 +452,8 @@ end
|
|
|
348
452
|
<BinaryString name="Tags"></BinaryString>
|
|
349
453
|
<ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
|
|
350
454
|
local InsertService = game:GetService("InsertService")
|
|
351
|
-
local
|
|
455
|
+
local rawId = tostring(data or ""):gsub("rbxassetid://", ""):gsub("%s+", "")
|
|
456
|
+
local assetId = tonumber(rawId)
|
|
352
457
|
if not assetId then
|
|
353
458
|
return { status = "error", error = "Asset ID harus berupa angka yang valid." }
|
|
354
459
|
end
|
|
@@ -358,8 +463,12 @@ end
|
|
|
358
463
|
end)
|
|
359
464
|
|
|
360
465
|
if success and model then
|
|
361
|
-
|
|
362
|
-
|
|
466
|
+
if targetInstance == game or targetInstance.ClassName == "DataModel" then
|
|
467
|
+
model.Parent = game.Workspace
|
|
468
|
+
else
|
|
469
|
+
model.Parent = targetInstance
|
|
470
|
+
end
|
|
471
|
+
return { status = "success", result = "Aset berhasil dimasukkan ke " .. model.Parent.Name .. "." }
|
|
363
472
|
else
|
|
364
473
|
return { status = "error", error = "Gagal memuat aset. Pastikan Asset ID benar dan akun/plugin Anda memiliki izin (Ownership/Public)." }
|
|
365
474
|
end
|
|
@@ -411,6 +520,7 @@ local ctx = {
|
|
|
411
520
|
recentLogs = {},
|
|
412
521
|
HttpService = HttpService,
|
|
413
522
|
studioId = STUDIO_ID,
|
|
523
|
+
getInstanceFromPath = nil -- akan di-bind di bawah
|
|
414
524
|
}
|
|
415
525
|
local MAX_LOGS = 50
|
|
416
526
|
local isRunning = false -- apakah loop aktif
|
|
@@ -423,6 +533,7 @@ local serverReachable = false -- apakah server Node.js reachable
|
|
|
423
533
|
local RECONNECT_DELAY_INITIAL = 1.5
|
|
424
534
|
local RECONNECT_DELAY_MAX = 1.5
|
|
425
535
|
local reconnectDelay = RECONNECT_DELAY_INITIAL
|
|
536
|
+
local currentLoopId = nil
|
|
426
537
|
|
|
427
538
|
-- ── Skills (ModuleScript) ─────────────────────────────────────────────────────
|
|
428
539
|
local skills = {}
|
|
@@ -502,21 +613,41 @@ LogService.MessageOut:Connect(addSystemLog)
|
|
|
502
613
|
|
|
503
614
|
-- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
|
|
504
615
|
local function getInstanceFromPath(pathString)
|
|
616
|
+
if type(pathString) ~= "string" or pathString == "" then return nil end
|
|
505
617
|
local parts = string.split(pathString, ".")
|
|
618
|
+
if #parts == 0 then return nil end
|
|
506
619
|
local current = game
|
|
507
620
|
if parts[1] == "game" then table.remove(parts, 1) end
|
|
508
621
|
pcall(function()
|
|
509
622
|
local svc = game:GetService(parts[1])
|
|
510
623
|
if svc then current = svc; table.remove(parts, 1) end
|
|
511
624
|
end)
|
|
512
|
-
|
|
625
|
+
local i = 1
|
|
626
|
+
while i <= #parts do
|
|
627
|
+
local part = parts[i]
|
|
513
628
|
local found = current:FindFirstChild(part)
|
|
629
|
+
|
|
630
|
+
-- Jika tidak ketemu, coba gabungkan dengan part berikutnya (untuk nama yang mengandung titik)
|
|
631
|
+
local combinedPart = part
|
|
632
|
+
local lookAhead = i + 1
|
|
633
|
+
while not found and lookAhead <= #parts do
|
|
634
|
+
combinedPart = combinedPart .. "." .. parts[lookAhead]
|
|
635
|
+
found = current:FindFirstChild(combinedPart)
|
|
636
|
+
if found then
|
|
637
|
+
i = lookAhead
|
|
638
|
+
end
|
|
639
|
+
lookAhead = lookAhead + 1
|
|
640
|
+
end
|
|
641
|
+
|
|
514
642
|
if not found then return nil end
|
|
515
643
|
current = found
|
|
644
|
+
i = i + 1
|
|
516
645
|
end
|
|
517
646
|
return current
|
|
518
647
|
end
|
|
519
648
|
|
|
649
|
+
ctx.getInstanceFromPath = getInstanceFromPath
|
|
650
|
+
|
|
520
651
|
-- ── Proses Task dari Server ───────────────────────────────────────────────────
|
|
521
652
|
local function processTask(taskData)
|
|
522
653
|
if not taskData or not taskData.id or not taskData.command then return nil end
|
|
@@ -527,7 +658,11 @@ local function processTask(taskData)
|
|
|
527
658
|
|
|
528
659
|
local targetInstance = getInstanceFromPath(taskData.target)
|
|
529
660
|
if not targetInstance then
|
|
530
|
-
|
|
661
|
+
if taskData.target == "" or taskData.target == "nil" then
|
|
662
|
+
targetInstance = game
|
|
663
|
+
else
|
|
664
|
+
return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
|
|
665
|
+
end
|
|
531
666
|
end
|
|
532
667
|
|
|
533
668
|
local skillFunc = skills[taskData.command]
|
|
@@ -535,8 +670,16 @@ local function processTask(taskData)
|
|
|
535
670
|
return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
|
|
536
671
|
end
|
|
537
672
|
|
|
538
|
-
local result =
|
|
539
|
-
|
|
673
|
+
local ok, result = pcall(function()
|
|
674
|
+
return skillFunc(targetInstance, taskData.data, ctx, taskData.target)
|
|
675
|
+
end)
|
|
676
|
+
|
|
677
|
+
if not ok then
|
|
678
|
+
result = { status = "error", error = "Internal Plugin Error: " .. tostring(result) }
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
result = result or { status = "success", result = "Eksekusi selesai (Tidak ada return value)" }
|
|
682
|
+
result.id = taskData.id
|
|
540
683
|
|
|
541
684
|
if ui then
|
|
542
685
|
local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
|
|
@@ -568,24 +711,11 @@ local function sendResponse(taskId, resultData)
|
|
|
568
711
|
end)
|
|
569
712
|
end
|
|
570
713
|
|
|
571
|
-
-- ── Update UI berdasarkan state ───────────────────────────────────────────────
|
|
572
|
-
local function updateUIStatus()
|
|
573
|
-
if not ui then return end
|
|
574
|
-
if not serverReachable then
|
|
575
|
-
ui:setStatus("waiting")
|
|
576
|
-
elseif mcpAgentActive then
|
|
577
|
-
ui:setStatus("connected")
|
|
578
|
-
else
|
|
579
|
-
-- Server reachable tapi tidak ada AI agent
|
|
580
|
-
-- Tampilkan sebagai connected (karena server sse bridge aktif)
|
|
581
|
-
ui:setStatus("server_connected")
|
|
582
|
-
end
|
|
583
|
-
end
|
|
584
|
-
|
|
585
714
|
-- ── Proses satu event dari SSE stream ────────────────────────────────────────
|
|
586
715
|
local function handleSSELine(line)
|
|
587
716
|
-- Format SSE: "data: {...json...}"
|
|
588
717
|
local payload = line
|
|
718
|
+
if line:sub(1, 6) == "event:" then return end
|
|
589
719
|
if line:sub(1, 5) == "data:" then
|
|
590
720
|
payload = line:sub(6):gsub("^%s+", ""):gsub("%s+$", "")
|
|
591
721
|
end
|
|
@@ -607,8 +737,12 @@ local function handleSSELine(line)
|
|
|
607
737
|
local changed = (mcpAgentActive ~= newActive)
|
|
608
738
|
mcpAgentActive = newActive
|
|
609
739
|
if ui then
|
|
610
|
-
|
|
611
|
-
|
|
740
|
+
if mcpAgentActive then
|
|
741
|
+
ui:setStatus("connected")
|
|
742
|
+
else
|
|
743
|
+
ui:setStatus("server_connected")
|
|
744
|
+
end
|
|
745
|
+
|
|
612
746
|
if newActive and changed then
|
|
613
747
|
ui:addLog("[System] ✅ IDE Agent (Antigravity) aktif.", Color3.fromRGB(100, 255, 100))
|
|
614
748
|
elseif not newActive and changed then
|
|
@@ -653,13 +787,16 @@ function startConnectionLoop()
|
|
|
653
787
|
isManualDisconnect = false
|
|
654
788
|
reconnectDelay = RECONNECT_DELAY_INITIAL
|
|
655
789
|
|
|
790
|
+
local loopId = HttpService:GenerateGUID(false)
|
|
791
|
+
currentLoopId = loopId
|
|
792
|
+
|
|
656
793
|
if ui then
|
|
657
794
|
ui:addLog("[System] NVStudio MCP v2.1.8 — Menghubungkan ke server...", Color3.fromRGB(200, 200, 200))
|
|
658
795
|
end
|
|
659
796
|
|
|
660
797
|
local consecutiveErrors = 0
|
|
661
798
|
|
|
662
|
-
while isRunning and not isManualDisconnect do
|
|
799
|
+
while isRunning and not isManualDisconnect and currentLoopId == loopId do
|
|
663
800
|
-- Stream / polling snapshot dari server
|
|
664
801
|
local streamOk, streamRaw = pcall(function()
|
|
665
802
|
return HttpService:GetAsync(
|
|
@@ -675,7 +812,7 @@ function startConnectionLoop()
|
|
|
675
812
|
if not serverReachable then
|
|
676
813
|
serverReachable = true
|
|
677
814
|
if ui then
|
|
678
|
-
ui:setStatus("connected")
|
|
815
|
+
ui:setStatus(mcpAgentActive and "connected" or "server_connected")
|
|
679
816
|
ui:addLog("[System] ✅ Server MCP terhubung & siap.", Color3.fromRGB(100, 255, 150))
|
|
680
817
|
end
|
|
681
818
|
end
|
|
@@ -765,6 +902,10 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
765
902
|
local replacementContent = payload.replacement_content
|
|
766
903
|
local allowMultiple = payload.allow_multiple or false
|
|
767
904
|
|
|
905
|
+
if targetContent == "" then
|
|
906
|
+
return { status = "error", error = "target_content tidak boleh berupa string kosong." }
|
|
907
|
+
end
|
|
908
|
+
|
|
768
909
|
-- Escape magic characters for string.gsub if needed, or use string.find manually.
|
|
769
910
|
-- string.gsub doesn't do plain text replacement directly without escaping.
|
|
770
911
|
-- We'll write a plain text replace function.
|
|
@@ -786,16 +927,28 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
786
927
|
return res, count
|
|
787
928
|
end
|
|
788
929
|
|
|
789
|
-
local
|
|
930
|
+
local function countOccurrences(str, pattern)
|
|
931
|
+
local count = 0
|
|
932
|
+
local startIdx = 1
|
|
933
|
+
while true do
|
|
934
|
+
local findStart, findEnd = string.find(str, pattern, startIdx, true)
|
|
935
|
+
if not findStart then break end
|
|
936
|
+
count = count + 1
|
|
937
|
+
startIdx = findEnd + 1
|
|
938
|
+
if count > 1 and not allowMultiple then break end
|
|
939
|
+
end
|
|
940
|
+
return count
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
local occurrences = countOccurrences(currentSource, targetContent)
|
|
790
944
|
|
|
791
945
|
if occurrences == 0 then
|
|
792
946
|
return { status = "error", error = "target_content tidak ditemukan di dalam script." }
|
|
793
947
|
elseif occurrences > 1 and not allowMultiple then
|
|
794
|
-
return { status = "error", error = "target_content ditemukan
|
|
948
|
+
return { status = "error", error = "target_content ditemukan lebih dari 1 kali. Tolong sertakan baris konteks lebih banyak agar unik, atau set allow_multiple = true." }
|
|
795
949
|
end
|
|
796
950
|
|
|
797
951
|
newSource, affectedLines = plainReplace(currentSource, targetContent, replacementContent)
|
|
798
|
-
affectedLines = affectedLines -- not exact line count, but occurrences count
|
|
799
952
|
|
|
800
953
|
-- Mode 2: Line Range Replace
|
|
801
954
|
elseif payload.start_line and payload.end_line and payload.replacement_content then
|
|
@@ -825,7 +978,11 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
825
978
|
end
|
|
826
979
|
|
|
827
980
|
-- Simpan backup snapshot sebelum mengaplikasikan patch
|
|
828
|
-
ctx.scriptHistory[targetPath]
|
|
981
|
+
if type(ctx.scriptHistory[targetPath]) ~= "table" then
|
|
982
|
+
ctx.scriptHistory[targetPath] = {}
|
|
983
|
+
end
|
|
984
|
+
table.insert(ctx.scriptHistory[targetPath], currentSource)
|
|
985
|
+
if #ctx.scriptHistory[targetPath] > 15 then table.remove(ctx.scriptHistory[targetPath], 1) end
|
|
829
986
|
|
|
830
987
|
-- Terapkan perubahan
|
|
831
988
|
targetInstance.Source = newSource
|
|
@@ -851,10 +1008,16 @@ end
|
|
|
851
1008
|
<BinaryString name="Tags"></BinaryString>
|
|
852
1009
|
<ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
|
|
853
1010
|
if targetInstance:IsA("LuaSourceContainer") then
|
|
854
|
-
local
|
|
855
|
-
if
|
|
1011
|
+
local historyStack = ctx.scriptHistory[targetPath]
|
|
1012
|
+
if historyStack and type(historyStack) == "table" and #historyStack > 0 then
|
|
1013
|
+
local prevSource = table.remove(historyStack)
|
|
856
1014
|
targetInstance.Source = prevSource
|
|
857
|
-
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback ke versi sebelumnya." }
|
|
1015
|
+
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback ke versi sebelumnya. Sisa backup: " .. #historyStack }
|
|
1016
|
+
elseif type(historyStack) == "string" then
|
|
1017
|
+
-- Kompatibilitas mundur dengan data lama (sebelum FEAT-04)
|
|
1018
|
+
targetInstance.Source = historyStack
|
|
1019
|
+
ctx.scriptHistory[targetPath] = nil
|
|
1020
|
+
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback (versi tunggal/legacy)." }
|
|
858
1021
|
else
|
|
859
1022
|
return { status = "error", error = "Tidak ada riwayat backup sebelumnya di sesi Studio ini untuk: " .. targetPath }
|
|
860
1023
|
end
|
|
@@ -876,10 +1039,10 @@ end
|
|
|
876
1039
|
<int64 name="SourceAssetId">-1</int64>
|
|
877
1040
|
<BinaryString name="Tags"></BinaryString>
|
|
878
1041
|
<ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
|
|
879
|
-
|
|
880
|
-
if not query or query == "" then
|
|
1042
|
+
if not data or data == "" or data == "nil" then
|
|
881
1043
|
return { status = "error", error = "Query pencarian tidak boleh kosong." }
|
|
882
1044
|
end
|
|
1045
|
+
local query = tostring(data)
|
|
883
1046
|
|
|
884
1047
|
local matches = {}
|
|
885
1048
|
local matchCount = 0
|
|
@@ -890,7 +1053,9 @@ end
|
|
|
890
1053
|
game:GetService("ServerScriptService"),
|
|
891
1054
|
game:GetService("ReplicatedStorage"),
|
|
892
1055
|
game:GetService("StarterPlayer"),
|
|
893
|
-
game:GetService("StarterGui")
|
|
1056
|
+
game:GetService("StarterGui"),
|
|
1057
|
+
game:GetService("ServerStorage"),
|
|
1058
|
+
game:GetService("Lighting")
|
|
894
1059
|
}
|
|
895
1060
|
|
|
896
1061
|
local function searchInScript(scriptObj)
|
|
@@ -920,19 +1085,32 @@ end
|
|
|
920
1085
|
return false
|
|
921
1086
|
end
|
|
922
1087
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1088
|
+
if targetInstance and targetInstance ~= game then
|
|
1089
|
+
local descendants = {}
|
|
1090
|
+
pcall(function() descendants = targetInstance:GetDescendants() end)
|
|
1091
|
+
for _, desc in ipairs(descendants) do
|
|
1092
|
+
if matchCount >= MAX_MATCHES then break end
|
|
1093
|
+
if desc:IsA("LuaSourceContainer") then
|
|
1094
|
+
if searchInScript(desc) then
|
|
1095
|
+
break
|
|
1096
|
+
end
|
|
1097
|
+
end
|
|
1098
|
+
end
|
|
1099
|
+
else
|
|
1100
|
+
for _, service in ipairs(searchableServices) do
|
|
1101
|
+
local descendants = {}
|
|
1102
|
+
pcall(function() descendants = service:GetDescendants() end)
|
|
926
1103
|
for _, desc in ipairs(descendants) do
|
|
1104
|
+
if matchCount >= MAX_MATCHES then break end
|
|
927
1105
|
if desc:IsA("LuaSourceContainer") then
|
|
928
1106
|
if searchInScript(desc) then
|
|
929
1107
|
break
|
|
930
1108
|
end
|
|
931
1109
|
end
|
|
932
1110
|
end
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1111
|
+
if matchCount >= MAX_MATCHES then
|
|
1112
|
+
break
|
|
1113
|
+
end
|
|
936
1114
|
end
|
|
937
1115
|
end
|
|
938
1116
|
|
|
@@ -952,16 +1130,35 @@ end
|
|
|
952
1130
|
<int64 name="SourceAssetId">-1</int64>
|
|
953
1131
|
<BinaryString name="Tags"></BinaryString>
|
|
954
1132
|
<ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
|
|
1133
|
+
if not data or data == "" or data == "nil" then
|
|
1134
|
+
return { status = "error", error = "Query pencarian tidak boleh kosong." }
|
|
1135
|
+
end
|
|
955
1136
|
local searchName = tostring(data)
|
|
956
1137
|
local results = {}
|
|
957
1138
|
|
|
958
1139
|
local searchableServices = {game:GetService("Workspace"), game:GetService("ReplicatedStorage"), game:GetService("ServerScriptService"), game:GetService("StarterGui")}
|
|
1140
|
+
local lowerSearch = string.lower(searchName)
|
|
959
1141
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1142
|
+
local nodesChecked = 0
|
|
1143
|
+
local function search(inst)
|
|
1144
|
+
nodesChecked = nodesChecked + 1
|
|
1145
|
+
if nodesChecked % 1000 == 0 then task.wait() end
|
|
1146
|
+
if string.find(inst.Name:lower(), searchName:lower(), 1, true) then
|
|
1147
|
+
table.insert(results, inst:GetFullName() .. " (" .. inst.ClassName .. ")")
|
|
1148
|
+
end
|
|
1149
|
+
if #results >= 50 then return end
|
|
1150
|
+
for _, child in ipairs(inst:GetChildren()) do
|
|
1151
|
+
search(child)
|
|
1152
|
+
if #results >= 50 then break end
|
|
1153
|
+
end
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
if targetInstance and targetInstance ~= game then
|
|
1157
|
+
search(targetInstance)
|
|
1158
|
+
else
|
|
1159
|
+
for _, service in ipairs(searchableServices) do
|
|
1160
|
+
search(service)
|
|
1161
|
+
if #results >= 50 then break end
|
|
965
1162
|
end
|
|
966
1163
|
end
|
|
967
1164
|
|
|
@@ -971,6 +1168,92 @@ end
|
|
|
971
1168
|
return { status = "success", result = "Tidak ditemukan instance dengan nama: " .. searchName }
|
|
972
1169
|
end
|
|
973
1170
|
end
|
|
1171
|
+
]]></ProtectedString>
|
|
1172
|
+
</Properties>
|
|
1173
|
+
</Item>
|
|
1174
|
+
<Item class="ModuleScript" referent="RBX_set_properties">
|
|
1175
|
+
<Properties>
|
|
1176
|
+
<BinaryString name="AttributesSerialize"></BinaryString>
|
|
1177
|
+
<SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
|
|
1178
|
+
<bool name="DefinesCapabilities">false</bool>
|
|
1179
|
+
<bool name="Disabled">false</bool>
|
|
1180
|
+
<Content name="LinkedSource"><null></null></Content>
|
|
1181
|
+
<string name="Name">set_properties</string>
|
|
1182
|
+
<int64 name="SourceAssetId">-1</int64>
|
|
1183
|
+
<BinaryString name="Tags"></BinaryString>
|
|
1184
|
+
<ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
|
|
1185
|
+
local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
|
|
1186
|
+
if not success or type(config) ~= "table" then
|
|
1187
|
+
return { status = "error", error = "Format data JSON tidak valid untuk set_properties." }
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
local applied = {}
|
|
1191
|
+
local errors = {}
|
|
1192
|
+
|
|
1193
|
+
for prop, val in pairs(config) do
|
|
1194
|
+
local ok, err = pcall(function()
|
|
1195
|
+
local currentVal = targetInstance[prop]
|
|
1196
|
+
if typeof(currentVal) == "Vector3" and type(val) == "table" and #val == 3 then
|
|
1197
|
+
targetInstance[prop] = Vector3.new(val[1], val[2], val[3])
|
|
1198
|
+
elseif typeof(currentVal) == "Color3" and type(val) == "table" and #val == 3 then
|
|
1199
|
+
local r, g, b = val[1], val[2], val[3]
|
|
1200
|
+
if r > 1 or g > 1 or b > 1 then
|
|
1201
|
+
targetInstance[prop] = Color3.fromRGB(r, g, b)
|
|
1202
|
+
else
|
|
1203
|
+
targetInstance[prop] = Color3.new(r, g, b)
|
|
1204
|
+
end
|
|
1205
|
+
elseif typeof(currentVal) == "UDim2" and type(val) == "table" and #val == 4 then
|
|
1206
|
+
targetInstance[prop] = UDim2.new(val[1], val[2], val[3], val[4])
|
|
1207
|
+
elseif typeof(currentVal) == "CFrame" and type(val) == "table" then
|
|
1208
|
+
targetInstance[prop] = CFrame.new(unpack(val))
|
|
1209
|
+
elseif typeof(currentVal) == "EnumItem" and type(val) == "string" then
|
|
1210
|
+
local enumType = tostring(currentVal.EnumType)
|
|
1211
|
+
local successEnum, enumValue = pcall(function() return Enum[enumType][val] end)
|
|
1212
|
+
if successEnum and enumValue then
|
|
1213
|
+
targetInstance[prop] = enumValue
|
|
1214
|
+
else
|
|
1215
|
+
error("Nilai Enum '" .. val .. "' tidak valid untuk EnumType '" .. enumType .. "'")
|
|
1216
|
+
end
|
|
1217
|
+
elseif prop == "Parent" or prop == "PrimaryPart" or string.match(prop, ".*Value$") then
|
|
1218
|
+
-- Evaluasi properti relasional
|
|
1219
|
+
if typeof(currentVal) == "Instance" or typeof(targetInstance[prop]) == "Instance" or val == nil then
|
|
1220
|
+
if type(val) == "string" and ctx.getInstanceFromPath then
|
|
1221
|
+
local resolved = ctx.getInstanceFromPath(val)
|
|
1222
|
+
if resolved then
|
|
1223
|
+
targetInstance[prop] = resolved
|
|
1224
|
+
else
|
|
1225
|
+
error("Instance target tidak ditemukan pada path: " .. val)
|
|
1226
|
+
end
|
|
1227
|
+
else
|
|
1228
|
+
targetInstance[prop] = val
|
|
1229
|
+
end
|
|
1230
|
+
else
|
|
1231
|
+
targetInstance[prop] = val
|
|
1232
|
+
end
|
|
1233
|
+
else
|
|
1234
|
+
targetInstance[prop] = val
|
|
1235
|
+
end
|
|
1236
|
+
end)
|
|
1237
|
+
|
|
1238
|
+
if ok then
|
|
1239
|
+
table.insert(applied, prop)
|
|
1240
|
+
else
|
|
1241
|
+
table.insert(errors, prop .. ": " .. tostring(err))
|
|
1242
|
+
end
|
|
1243
|
+
end
|
|
1244
|
+
|
|
1245
|
+
if #errors > 0 then
|
|
1246
|
+
local successMsg = ""
|
|
1247
|
+
if #applied > 0 then
|
|
1248
|
+
successMsg = "Berhasil mengubah: " .. table.concat(applied, ", ") .. ".\n"
|
|
1249
|
+
end
|
|
1250
|
+
return {
|
|
1251
|
+
status = #applied > 0 and "success" or "error",
|
|
1252
|
+
result = successMsg .. "Namun beberapa properti gagal diubah:\n" .. table.concat(errors, "\n")
|
|
1253
|
+
}
|
|
1254
|
+
end
|
|
1255
|
+
return { status = "success", result = "Berhasil mengubah properti: " .. table.concat(applied, ", ") }
|
|
1256
|
+
end
|
|
974
1257
|
]]></ProtectedString>
|
|
975
1258
|
</Properties>
|
|
976
1259
|
</Item>
|
|
@@ -1081,7 +1364,7 @@ function UIManager.init(pluginInstance, studioId)
|
|
|
1081
1364
|
|
|
1082
1365
|
-- Version
|
|
1083
1366
|
local versionLabel = Instance.new("TextLabel")
|
|
1084
|
-
versionLabel.Text = "v2.
|
|
1367
|
+
versionLabel.Text = "v2.2.0"
|
|
1085
1368
|
versionLabel.Font = Enum.Font.Gotham
|
|
1086
1369
|
versionLabel.TextSize = 11
|
|
1087
1370
|
versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)
|
|
@@ -1405,9 +1688,9 @@ function UIManager:setStatus(stateStr)
|
|
|
1405
1688
|
-- Server reachable, tampilkan Connected meskipun agent (IDE) belum konek via stdio
|
|
1406
1689
|
self.connectBtn.Text = " Disconnect"
|
|
1407
1690
|
self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
|
|
1408
|
-
self.statusLabel.Text = "●
|
|
1409
|
-
self.statusLabel.TextColor3 = Color3.fromRGB(
|
|
1410
|
-
self.termStroke.Color = Color3.fromRGB(
|
|
1691
|
+
self.statusLabel.Text = "● IDE Disconnected"
|
|
1692
|
+
self.statusLabel.TextColor3 = Color3.fromRGB(255, 180, 50)
|
|
1693
|
+
self.termStroke.Color = Color3.fromRGB(120, 80, 20)
|
|
1411
1694
|
|
|
1412
1695
|
elseif stateStr == "connected" then
|
|
1413
1696
|
self.connectBtn.Text = " Disconnect"
|
|
@@ -1458,7 +1741,11 @@ return UIManager
|
|
|
1458
1741
|
-- hanya akan membuat AI gagal terus menerus untuk menulis script.
|
|
1459
1742
|
-- Error syntax akan ditangkap secara native oleh Roblox Output.
|
|
1460
1743
|
|
|
1461
|
-
ctx.scriptHistory[targetPath]
|
|
1744
|
+
if type(ctx.scriptHistory[targetPath]) ~= "table" then
|
|
1745
|
+
ctx.scriptHistory[targetPath] = {}
|
|
1746
|
+
end
|
|
1747
|
+
table.insert(ctx.scriptHistory[targetPath], targetInstance.Source)
|
|
1748
|
+
if #ctx.scriptHistory[targetPath] > 15 then table.remove(ctx.scriptHistory[targetPath], 1) end
|
|
1462
1749
|
targetInstance.Source = newCode
|
|
1463
1750
|
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil diperbarui." }
|
|
1464
1751
|
else
|