@adiholick/nvstudio-mcp 1.0.4 → 1.1.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/dist/index.js +36 -2
- package/package.json +3 -2
- package/studio-plugin/Main.server.lua +291 -0
- package/studio-plugin/create_instance.lua +24 -0
- package/studio-plugin/delete_instance.lua +31 -0
- package/studio-plugin/execute_luau.lua +71 -0
- package/studio-plugin/generate_terrain.lua +23 -0
- package/studio-plugin/get_children.lua +7 -0
- package/studio-plugin/get_logs.lua +7 -0
- package/studio-plugin/get_properties.lua +52 -0
- package/studio-plugin/get_script_source.lua +7 -0
- package/studio-plugin/insert_asset.lua +18 -0
- package/studio-plugin/nvstudio_mcp.rbxmx +1337 -0
- package/studio-plugin/rollback_script.lua +13 -0
- package/studio-plugin/script_grep.lua +63 -0
- package/studio-plugin/search_instance.lua +20 -0
- package/studio-plugin/ui_manager.lua +443 -0
- package/studio-plugin/update_script_source.lua +16 -0
package/dist/index.js
CHANGED
|
@@ -38,7 +38,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
|
38
38
|
tools: [
|
|
39
39
|
{
|
|
40
40
|
name: "call_mcp_tool",
|
|
41
|
-
description: "ALAT MUTLAK UNTUK ROBLOX. DILARANG KERAS menggunakan PowerShell atau skrip bypass (fetch_mcp_source.js). WAJIB memanggil 'get_children' terlebih dahulu sebelum 'get_script_source'
|
|
41
|
+
description: "ALAT MUTLAK UNTUK ROBLOX. DILARANG KERAS menggunakan PowerShell atau skrip bypass (fetch_mcp_source.js). WAJIB memanggil 'get_children' terlebih dahulu sebelum 'get_script_source'. Command: get_children, get_script_source, update_script_source, rollback_script, get_logs, create_instance, search_instance, generate_terrain, insert_asset, delete_instance, get_properties, script_grep, execute_luau, search_asset.",
|
|
42
42
|
inputSchema: {
|
|
43
43
|
type: "object",
|
|
44
44
|
properties: {
|
|
@@ -95,7 +95,41 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
// 7.
|
|
98
|
+
// 7. Intercept command yang dieksekusi di server lokal (Node.js) alih-alih di Studio
|
|
99
|
+
if (command === "search_asset" && data) {
|
|
100
|
+
try {
|
|
101
|
+
const queryParams = typeof data === "string" ? JSON.parse(data) : data;
|
|
102
|
+
const keyword = encodeURIComponent(queryParams.keyword || "");
|
|
103
|
+
let assetType = "10"; // Default: Model
|
|
104
|
+
if (queryParams.category === "Audio") {
|
|
105
|
+
assetType = "9"; // Audio
|
|
106
|
+
}
|
|
107
|
+
else if (queryParams.category === "Decal" || queryParams.category === "Image") {
|
|
108
|
+
assetType = "13"; // Decal
|
|
109
|
+
}
|
|
110
|
+
const url = `https://catalog.roblox.com/v1/search/items?category=All&keyword=${keyword}&limit=10&itemTypes=Asset&assetTypes=${assetType}`;
|
|
111
|
+
const res = await fetch(url);
|
|
112
|
+
const jsonRes = await res.json();
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: JSON.stringify({
|
|
118
|
+
status: "success",
|
|
119
|
+
result: jsonRes.data || jsonRes
|
|
120
|
+
}, null, 2)
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text: JSON.stringify({ status: "error", error: "Gagal mencari aset: " + err.message }) }],
|
|
128
|
+
isError: true
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 8. Teruskan ke antrean dan kembalikan hasilnya sebagai string JSON
|
|
99
133
|
try {
|
|
100
134
|
if (command === "update_script_source" && data) {
|
|
101
135
|
backupScript(target, String(data));
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adiholick/nvstudio-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Local Bridge Server MCP for Roblox Studio",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": "dist/index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
|
-
"public"
|
|
9
|
+
"public",
|
|
10
|
+
"studio-plugin"
|
|
10
11
|
],
|
|
11
12
|
"scripts": {
|
|
12
13
|
"build": "tsc",
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
local HttpService = game:GetService("HttpService")
|
|
2
|
+
local LogService = game:GetService("LogService")
|
|
3
|
+
|
|
4
|
+
local TASK_URL = "http://localhost:3055/api/tasks"
|
|
5
|
+
local RESPONSE_URL = "http://localhost:3055/api/response"
|
|
6
|
+
|
|
7
|
+
-- Generate Unique Studio ID (menghindari bentrok jika 2 Studio terbuka)
|
|
8
|
+
local STUDIO_ID = HttpService:GenerateGUID(false)
|
|
9
|
+
|
|
10
|
+
-- Import UI Manager (Procedural Dashboard)
|
|
11
|
+
local uiModule = script.Parent:FindFirstChild("ui_manager")
|
|
12
|
+
local ui = nil
|
|
13
|
+
|
|
14
|
+
-- Membangun Tombol Toolbar (Toggle)
|
|
15
|
+
local toolbar = plugin:CreateToolbar("NVStudio MCP")
|
|
16
|
+
local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722")
|
|
17
|
+
|
|
18
|
+
-- Global State & Context AI
|
|
19
|
+
local ctx = {
|
|
20
|
+
scriptHistory = {},
|
|
21
|
+
recentLogs = {},
|
|
22
|
+
HttpService = HttpService,
|
|
23
|
+
studioId = STUDIO_ID
|
|
24
|
+
}
|
|
25
|
+
local MAX_LOGS = 50
|
|
26
|
+
local isConnected = false
|
|
27
|
+
local autoConnect = plugin:GetSetting("AutoConnect") or false
|
|
28
|
+
local isPolling = false
|
|
29
|
+
local isClickDebounce = false -- Mencegah rapid double-click
|
|
30
|
+
|
|
31
|
+
-- Deklarasi fungsi polling di awal agar bisa dipanggil oleh UI
|
|
32
|
+
local startPolling
|
|
33
|
+
local stopPolling
|
|
34
|
+
|
|
35
|
+
-- Inisialisasi UI jika modul ditemukan
|
|
36
|
+
if uiModule then
|
|
37
|
+
local UIManager = require(uiModule)
|
|
38
|
+
ui = UIManager.init(plugin, STUDIO_ID)
|
|
39
|
+
|
|
40
|
+
-- Sync Initial State ke UI
|
|
41
|
+
ui:setAutoConnectState(autoConnect)
|
|
42
|
+
ui:setStatus("disconnected")
|
|
43
|
+
|
|
44
|
+
-- Event Toggle GUI Panel
|
|
45
|
+
toggleBtn.Click:Connect(function()
|
|
46
|
+
ui:toggleVisibility()
|
|
47
|
+
end)
|
|
48
|
+
|
|
49
|
+
-- Event Tombol Connect (dengan debounce untuk mencegah double-click)
|
|
50
|
+
ui.connectBtn.MouseButton1Click:Connect(function()
|
|
51
|
+
if isClickDebounce then return end
|
|
52
|
+
isClickDebounce = true
|
|
53
|
+
|
|
54
|
+
if not isConnected then
|
|
55
|
+
-- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
|
|
56
|
+
isManualDisconnect = false
|
|
57
|
+
isConnected = true
|
|
58
|
+
ui:setStatus("waiting")
|
|
59
|
+
ui:addLog("[System] Koneksi manual dimulai. Mencari AI Server (Waiting)...", Color3.fromRGB(255, 220, 100))
|
|
60
|
+
startPolling()
|
|
61
|
+
else
|
|
62
|
+
-- DISCONNECT — hentikan polling dan tandai manual disconnect
|
|
63
|
+
stopPolling()
|
|
64
|
+
ui:setStatus("disconnected")
|
|
65
|
+
ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
task.delay(0.3, function()
|
|
69
|
+
isClickDebounce = false
|
|
70
|
+
end)
|
|
71
|
+
end)
|
|
72
|
+
|
|
73
|
+
-- Event Custom Checkbox (Auto-Connect)
|
|
74
|
+
ui.autoConnectBtn.MouseButton1Click:Connect(function()
|
|
75
|
+
autoConnect = not autoConnect
|
|
76
|
+
plugin:SetSetting("AutoConnect", autoConnect)
|
|
77
|
+
ui:setAutoConnectState(autoConnect)
|
|
78
|
+
ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
|
|
79
|
+
end)
|
|
80
|
+
else
|
|
81
|
+
-- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
|
|
82
|
+
toggleBtn.Click:Connect(function()
|
|
83
|
+
print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan di dalam Folder. GUI Gagal dimuat!")
|
|
84
|
+
end)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
-- Handler Output Studio (Untuk AI dan GUI)
|
|
88
|
+
local function addSystemLog(msg, msgType)
|
|
89
|
+
local color = Color3.fromRGB(200, 200, 200)
|
|
90
|
+
local prefix = "[INFO] "
|
|
91
|
+
if msgType == Enum.MessageType.MessageError then
|
|
92
|
+
prefix = "[ERROR] "
|
|
93
|
+
color = Color3.fromRGB(255, 100, 100)
|
|
94
|
+
elseif msgType == Enum.MessageType.MessageWarning then
|
|
95
|
+
prefix = "[WARN] "
|
|
96
|
+
color = Color3.fromRGB(255, 200, 100)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
local fullMsg = prefix .. msg
|
|
100
|
+
|
|
101
|
+
-- 1. Simpan di memori untuk disedot AI
|
|
102
|
+
table.insert(ctx.recentLogs, fullMsg)
|
|
103
|
+
if #ctx.recentLogs > MAX_LOGS then
|
|
104
|
+
table.remove(ctx.recentLogs, 1)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
-- 2. Cetak ke Terminal GUI Plugin
|
|
108
|
+
if ui then
|
|
109
|
+
ui:addLog(fullMsg, color)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
-- Ikat listener output
|
|
114
|
+
LogService.MessageOut:Connect(addSystemLog)
|
|
115
|
+
|
|
116
|
+
-- Memuat semua module skills secara dinamis
|
|
117
|
+
local skills = {}
|
|
118
|
+
for _, module in ipairs(script.Parent:GetChildren()) do
|
|
119
|
+
if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
|
|
120
|
+
skills[module.Name] = require(module)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
-- Fungsi Helper Pathing
|
|
125
|
+
local function getInstanceFromPath(pathString)
|
|
126
|
+
local parts = string.split(pathString, ".")
|
|
127
|
+
local current = game
|
|
128
|
+
|
|
129
|
+
if parts[1] == "game" then table.remove(parts, 1) end
|
|
130
|
+
|
|
131
|
+
for _, part in ipairs(parts) do
|
|
132
|
+
local found = current:FindFirstChild(part)
|
|
133
|
+
if not found then return nil end
|
|
134
|
+
current = found
|
|
135
|
+
end
|
|
136
|
+
return current
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
-- Memproses Task Eksekusi dari AI
|
|
140
|
+
local function processTask(taskData)
|
|
141
|
+
if not taskData or not taskData.id or not taskData.command then return nil end
|
|
142
|
+
|
|
143
|
+
if ui then
|
|
144
|
+
ui:addLog("[AI Command] Menerima instruksi: " .. taskData.command, Color3.fromRGB(150, 200, 255))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
local targetInstance = getInstanceFromPath(taskData.target)
|
|
148
|
+
if not targetInstance then
|
|
149
|
+
return {
|
|
150
|
+
id = taskData.id,
|
|
151
|
+
status = "error",
|
|
152
|
+
error = "Target path tidak ditemukan: " .. taskData.target
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
local skillFunc = skills[taskData.command]
|
|
157
|
+
if not skillFunc then
|
|
158
|
+
return {
|
|
159
|
+
id = taskData.id,
|
|
160
|
+
status = "error",
|
|
161
|
+
error = "Command tidak dikenali oleh Studio: " .. taskData.command
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
-- Eksekusi skill AI
|
|
166
|
+
local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
|
|
167
|
+
result.id = taskData.id
|
|
168
|
+
|
|
169
|
+
-- Cetak hasil eksekusi ke Terminal GUI
|
|
170
|
+
if ui then
|
|
171
|
+
local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
|
|
172
|
+
ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
-- Menghentikan polling dengan bersih (dipanggil oleh tombol Disconnect)
|
|
179
|
+
local isManualDisconnect = false -- Mencegah auto-reconnect saat user sengaja disconnect
|
|
180
|
+
|
|
181
|
+
function stopPolling()
|
|
182
|
+
isConnected = false
|
|
183
|
+
isManualDisconnect = true
|
|
184
|
+
-- isPolling akan menjadi false secara otomatis saat loop keluar
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
-- Background Polling Loop (Dapat dihentikan oleh user)
|
|
188
|
+
function startPolling()
|
|
189
|
+
if isPolling then return end
|
|
190
|
+
isPolling = true
|
|
191
|
+
|
|
192
|
+
task.spawn(function()
|
|
193
|
+
while isConnected do
|
|
194
|
+
local success, response = pcall(function()
|
|
195
|
+
-- Tambahkan cache-buster agar Studio tidak me-cache response HTTP
|
|
196
|
+
local cacheBuster = "?t=" .. tostring(os.clock())
|
|
197
|
+
return HttpService:GetAsync(TASK_URL .. cacheBuster)
|
|
198
|
+
end)
|
|
199
|
+
|
|
200
|
+
-- PENTING: Cek ulang isConnected SETELAH HTTP request selesai.
|
|
201
|
+
-- Jika user menekan Disconnect saat request sedang berjalan,
|
|
202
|
+
-- kita TIDAK boleh menimpa status UI "disconnected" yang sudah di-set.
|
|
203
|
+
if not isConnected then
|
|
204
|
+
break
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if success and response then
|
|
208
|
+
-- JIKA PING SUKSES, Node.js menyala (IDE / Agen Sedang Terbuka!)
|
|
209
|
+
if ui then ui:setStatus("connected") end
|
|
210
|
+
|
|
211
|
+
local decodeSuccess, taskData = pcall(function()
|
|
212
|
+
return HttpService:JSONDecode(response)
|
|
213
|
+
end)
|
|
214
|
+
|
|
215
|
+
if decodeSuccess and taskData and taskData.id then
|
|
216
|
+
local resultData = processTask(taskData)
|
|
217
|
+
if resultData then
|
|
218
|
+
pcall(function()
|
|
219
|
+
local jsonResult = HttpService:JSONEncode(resultData)
|
|
220
|
+
HttpService:PostAsync(RESPONSE_URL, jsonResult, Enum.HttpContentType.ApplicationJson)
|
|
221
|
+
end)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
else
|
|
225
|
+
-- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
|
|
226
|
+
if ui then ui:setStatus("waiting") end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
-- Cek lagi sebelum sleep (agar disconnect lebih responsif)
|
|
230
|
+
if not isConnected then
|
|
231
|
+
break
|
|
232
|
+
end
|
|
233
|
+
task.wait(1)
|
|
234
|
+
end
|
|
235
|
+
isPolling = false
|
|
236
|
+
end)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
240
|
+
-- AUTO-DETECT: Background heartbeat yang SELALU berjalan.
|
|
241
|
+
-- Mendeteksi apakah server MCP aktif setiap 2 detik.
|
|
242
|
+
-- Jika terdeteksi aktif → otomatis connect (tanpa perlu tekan tombol).
|
|
243
|
+
-- Jika terdeteksi mati → otomatis update status.
|
|
244
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
245
|
+
task.spawn(function()
|
|
246
|
+
task.wait(1) -- Tunggu sebentar agar UI siap
|
|
247
|
+
|
|
248
|
+
if ui then
|
|
249
|
+
ui:addLog("[System] Plugin termuat. Auto-detect server MCP aktif...", Color3.fromRGB(200, 200, 200))
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
while true do
|
|
253
|
+
-- Skip heartbeat jika polling loop sudah aktif (ia sudah menangani semuanya)
|
|
254
|
+
if not isPolling then
|
|
255
|
+
local success, errorMsg = pcall(function()
|
|
256
|
+
local cacheBuster = "?t=" .. tostring(os.clock())
|
|
257
|
+
return HttpService:GetAsync(TASK_URL .. cacheBuster)
|
|
258
|
+
end)
|
|
259
|
+
|
|
260
|
+
if success then
|
|
261
|
+
-- Server terdeteksi aktif!
|
|
262
|
+
if not isConnected and not isManualDisconnect then
|
|
263
|
+
isConnected = true
|
|
264
|
+
if ui then
|
|
265
|
+
ui:setStatus("connected")
|
|
266
|
+
ui:addLog("[System] Server MCP terdeteksi! Auto-connected.", Color3.fromRGB(100, 255, 100))
|
|
267
|
+
end
|
|
268
|
+
startPolling()
|
|
269
|
+
end
|
|
270
|
+
else
|
|
271
|
+
-- Server tidak aktif
|
|
272
|
+
if isConnected then
|
|
273
|
+
-- Server baru saja mati (sebelumnya connected) — polling loop juga akan exit
|
|
274
|
+
isConnected = false
|
|
275
|
+
if ui then
|
|
276
|
+
ui:setStatus("waiting")
|
|
277
|
+
ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
|
|
278
|
+
end
|
|
279
|
+
elseif not isManualDisconnect then
|
|
280
|
+
if ui then
|
|
281
|
+
ui:setStatus("waiting")
|
|
282
|
+
-- HANYA print error jika belum terkoneksi, untuk debugging
|
|
283
|
+
ui:addLog("[Debug] Deteksi gagal: " .. tostring(errorMsg), Color3.fromRGB(150, 150, 150))
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
task.wait(2)
|
|
290
|
+
end
|
|
291
|
+
end)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
|
|
3
|
+
if not success or not config.ClassName then
|
|
4
|
+
return { status = "error", error = "Format data JSON tidak valid atau ClassName hilang." }
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
local newSuccess, newInst = pcall(function()
|
|
8
|
+
local inst = Instance.new(config.ClassName)
|
|
9
|
+
inst.Parent = targetInstance
|
|
10
|
+
|
|
11
|
+
if config.Properties then
|
|
12
|
+
for prop, val in pairs(config.Properties) do
|
|
13
|
+
pcall(function() inst[prop] = val end)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
return inst
|
|
17
|
+
end)
|
|
18
|
+
|
|
19
|
+
if newSuccess then
|
|
20
|
+
return { status = "success", result = "Instance '"..config.ClassName.."' berhasil dibuat di " .. targetPath }
|
|
21
|
+
else
|
|
22
|
+
return { status = "error", error = "Gagal membuat Instance. Pastikan ClassName valid." }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
-- Guardrail: Cegah penghapusan Service utama
|
|
3
|
+
local protectedServices = {
|
|
4
|
+
"Workspace", "Players", "Lighting", "MaterialService", "NetworkClient",
|
|
5
|
+
"ReplicatedFirst", "ReplicatedStorage", "ServerScriptService", "ServerStorage",
|
|
6
|
+
"StarterGui", "StarterPack", "StarterPlayer", "Teams", "SoundService", "Chat",
|
|
7
|
+
"TextChatService", "VoiceChatService"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if targetInstance.Parent == game or targetInstance.Parent == nil then
|
|
11
|
+
for _, serviceName in ipairs(protectedServices) do
|
|
12
|
+
if targetInstance.Name == serviceName or targetInstance.ClassName == serviceName then
|
|
13
|
+
return { status = "error", error = "GUARDRAIL BLOCKED: Anda tidak diizinkan untuk menghapus layanan tingkat atas (" .. targetInstance.Name .. ")." }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
local name = targetInstance.Name
|
|
19
|
+
local className = targetInstance.ClassName
|
|
20
|
+
local parentName = targetInstance.Parent and targetInstance.Parent.Name or "nil"
|
|
21
|
+
|
|
22
|
+
local success, err = pcall(function()
|
|
23
|
+
targetInstance:Destroy()
|
|
24
|
+
end)
|
|
25
|
+
|
|
26
|
+
if success then
|
|
27
|
+
return { status = "success", result = "Instance '" .. name .. "' (" .. className .. ") berhasil dihapus dari " .. parentName .. "." }
|
|
28
|
+
else
|
|
29
|
+
return { status = "error", error = "Gagal menghapus instance: " .. tostring(err) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local code = data
|
|
3
|
+
if not code or code == "" then
|
|
4
|
+
return { status = "error", error = "Kode yang dieksekusi tidak boleh kosong." }
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
local HttpService = game:GetService("HttpService")
|
|
8
|
+
|
|
9
|
+
-- Bungkus code ke dalam fungsi return
|
|
10
|
+
local wrappedCode = "return function()\n" .. code .. "\nend"
|
|
11
|
+
|
|
12
|
+
local moduleScript = Instance.new("ModuleScript")
|
|
13
|
+
moduleScript.Name = "AI_Execute_Temp"
|
|
14
|
+
moduleScript.Source = wrappedCode
|
|
15
|
+
|
|
16
|
+
-- Taruh di ReplicatedStorage sementara agar bisa di-require
|
|
17
|
+
moduleScript.Parent = game:GetService("ReplicatedStorage")
|
|
18
|
+
|
|
19
|
+
local resultData = nil
|
|
20
|
+
local execError = nil
|
|
21
|
+
local isFinished = false
|
|
22
|
+
|
|
23
|
+
-- Eksekusi asinkron dengan batas waktu (timeout) 5 detik
|
|
24
|
+
task.spawn(function()
|
|
25
|
+
local reqSuccess, func = pcall(function() return require(moduleScript) end)
|
|
26
|
+
if reqSuccess and type(func) == "function" then
|
|
27
|
+
local execSuccess, res = pcall(func)
|
|
28
|
+
if execSuccess then
|
|
29
|
+
resultData = res
|
|
30
|
+
else
|
|
31
|
+
execError = "Runtime Error: " .. tostring(res)
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
execError = "Syntax/Require Error: " .. tostring(func)
|
|
35
|
+
end
|
|
36
|
+
isFinished = true
|
|
37
|
+
end)
|
|
38
|
+
|
|
39
|
+
-- Tunggu hingga 5 detik
|
|
40
|
+
local timeWaited = 0
|
|
41
|
+
while not isFinished and timeWaited < 5 do
|
|
42
|
+
timeWaited = timeWaited + task.wait()
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
-- Pembersihan (Wajib dihancurkan)
|
|
46
|
+
pcall(function() moduleScript:Destroy() end)
|
|
47
|
+
|
|
48
|
+
if not isFinished then
|
|
49
|
+
return { status = "error", error = "Timeout 5 detik terlampaui. Kemungkinan ada infinite loop tanpa task.wait()." }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if execError then
|
|
53
|
+
return { status = "error", error = execError }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
-- Cegah return nil error
|
|
57
|
+
if resultData == nil then
|
|
58
|
+
return { status = "success", result = "Eksekusi berhasil (Tidak ada nilai kembalian)." }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
-- JSON Encode untuk mencegah raw Instance return crash
|
|
62
|
+
local serializeSuccess, jsonRes = pcall(function()
|
|
63
|
+
return HttpService:JSONEncode(resultData)
|
|
64
|
+
end)
|
|
65
|
+
|
|
66
|
+
if serializeSuccess then
|
|
67
|
+
return { status = "success", result = HttpService:JSONDecode(jsonRes) } -- decode kembali agar format aslinya diteruskan oleh router
|
|
68
|
+
else
|
|
69
|
+
return { status = "error", error = "Gagal menserialisasi hasil (Dilarang mengembalikan raw Roblox Instance): " .. tostring(jsonRes) }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
|
|
3
|
+
if not success or not config.Size or not config.Position or not config.Material then
|
|
4
|
+
return { status = "error", error = "Format JSON tidak valid. Membutuhkan Size, Position, dan Material." }
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
local fillSuccess, err = pcall(function()
|
|
8
|
+
local terrain = game.Workspace.Terrain
|
|
9
|
+
local materialEnum = Enum.Material[config.Material] or Enum.Material.Grass
|
|
10
|
+
|
|
11
|
+
local size = Vector3.new(config.Size[1], config.Size[2], config.Size[3])
|
|
12
|
+
local position = Vector3.new(config.Position[1], config.Position[2], config.Position[3])
|
|
13
|
+
|
|
14
|
+
local region = Region3.new(position - (size/2), position + (size/2))
|
|
15
|
+
terrain:FillRegion(region:ExpandToGrid(4), 4, materialEnum)
|
|
16
|
+
end)
|
|
17
|
+
|
|
18
|
+
if fillSuccess then
|
|
19
|
+
return { status = "success", result = "Terrain (" .. config.Material .. ") berhasil di-generate." }
|
|
20
|
+
else
|
|
21
|
+
return { status = "error", error = "Gagal men-generate Terrain. Pastikan tipe data array angka valid: " .. tostring(err) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local children = {}
|
|
3
|
+
for _, child in ipairs(targetInstance:GetChildren()) do
|
|
4
|
+
table.insert(children, child.Name .. " (" .. child.ClassName .. ")")
|
|
5
|
+
end
|
|
6
|
+
return { status = "success", result = children }
|
|
7
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local CollectionService = game:GetService("CollectionService")
|
|
3
|
+
|
|
4
|
+
local props = {
|
|
5
|
+
Name = targetInstance.Name,
|
|
6
|
+
ClassName = targetInstance.ClassName,
|
|
7
|
+
Parent = targetInstance.Parent and targetInstance.Parent.Name or "nil",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
-- Ambil Properties Umum secara aman via pcall
|
|
11
|
+
local function tryProp(propName)
|
|
12
|
+
local success, val = pcall(function() return targetInstance[propName] end)
|
|
13
|
+
if success and val ~= nil then
|
|
14
|
+
if typeof(val) == "Vector3" then
|
|
15
|
+
props[propName] = {math.round(val.X*100)/100, math.round(val.Y*100)/100, math.round(val.Z*100)/100}
|
|
16
|
+
elseif typeof(val) == "Color3" then
|
|
17
|
+
props[propName] = {math.round(val.R * 255), math.round(val.G * 255), math.round(val.B * 255)}
|
|
18
|
+
elseif typeof(val) == "CFrame" then
|
|
19
|
+
props[propName] = tostring(val)
|
|
20
|
+
elseif typeof(val) == "EnumItem" then
|
|
21
|
+
props[propName] = val.Name
|
|
22
|
+
elseif typeof(val) == "UDim2" then
|
|
23
|
+
props[propName] = {val.X.Scale, val.X.Offset, val.Y.Scale, val.Y.Offset}
|
|
24
|
+
elseif typeof(val) == "Instance" then
|
|
25
|
+
props[propName] = val:GetFullName()
|
|
26
|
+
else
|
|
27
|
+
props[propName] = val
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
local commonProps = {
|
|
33
|
+
"Anchored", "CanCollide", "Transparency", "Reflectance",
|
|
34
|
+
"Position", "Size", "Color", "Material", "Text", "TextColor3",
|
|
35
|
+
"BackgroundTransparency", "BackgroundColor3", "Visible", "Enabled",
|
|
36
|
+
"Volume", "Playing", "Looped", "Value"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for _, prop in ipairs(commonProps) do
|
|
40
|
+
tryProp(prop)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
-- Attributes & Tags
|
|
44
|
+
pcall(function()
|
|
45
|
+
props.Attributes = targetInstance:GetAttributes()
|
|
46
|
+
end)
|
|
47
|
+
pcall(function()
|
|
48
|
+
props.Tags = CollectionService:GetTags(targetInstance)
|
|
49
|
+
end)
|
|
50
|
+
|
|
51
|
+
return { status = "success", result = props }
|
|
52
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
if targetInstance:IsA("LuaSourceContainer") then
|
|
3
|
+
return { status = "success", result = targetInstance.Source }
|
|
4
|
+
else
|
|
5
|
+
return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. "). Kamu WAJIB menggunakan get_children terlebih dahulu sebelum menarik source." }
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local InsertService = game:GetService("InsertService")
|
|
3
|
+
local assetId = tonumber(data)
|
|
4
|
+
if not assetId then
|
|
5
|
+
return { status = "error", error = "Asset ID harus berupa angka yang valid." }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
local success, model = pcall(function()
|
|
9
|
+
return InsertService:LoadAsset(assetId)
|
|
10
|
+
end)
|
|
11
|
+
|
|
12
|
+
if success and model then
|
|
13
|
+
model.Parent = game.Workspace
|
|
14
|
+
return { status = "success", result = "Aset berhasil dimasukkan ke Workspace." }
|
|
15
|
+
else
|
|
16
|
+
return { status = "error", error = "Gagal memuat aset. Pastikan Asset ID benar dan akun/plugin Anda memiliki izin (Ownership/Public)." }
|
|
17
|
+
end
|
|
18
|
+
end
|