@adiholick/nvstudio-mcp 2.2.0 → 2.2.2
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 +9 -3
- package/dist/index.js +22 -0
- package/dist/roblox-bridge.js +128 -31
- package/package.json +1 -1
- package/studio-plugin/Main.server.lua +1 -1
- package/studio-plugin/nvstudio_mcp.rbxmx +5 -3
- package/studio-plugin/script_grep.lua +4 -2
package/README.md
CHANGED
|
@@ -32,8 +32,8 @@ curl -fsSL https://raw.githubusercontent.com/Adiholick/nvstudio-mcp/main/install
|
|
|
32
32
|
**Apa saja yang dilakukan installer ini secara otomatis?**
|
|
33
33
|
1. 📦 Mengunduh source code, memasang dependensi Node.js, dan mengkompilasi TypeScript (*Bridge Server*).
|
|
34
34
|
2. 🧩 **Memasang Plugin Roblox Studio (`nvstudio_mcp.rbxmx`) otomatis** ke folder sistem Roblox Studio Anda (`%LOCALAPPDATA%\Roblox\Plugins` di Windows atau `~/Documents/ROBLOX/Plugins` di macOS).
|
|
35
|
-
3.
|
|
36
|
-
4. ⚙️ Melakukan auto-konfigurasi jika mendeteksi editor **Cursor** (`.cursor/mcp.json`).
|
|
35
|
+
3. 🧠 **Universal AI Skill Installer**: Secara cerdas memindai dan mengunduh library skill 29+ *roblox-brain* dan *nvstudio-mcp-guide*, lalu menginjeksinya langsung ke dalam struktur `skills/` dari **AI Agent apapun** yang Anda gunakan (mendukung **Antigravity IDE**, **Cursor**, **Claude Code**, **Codex**, **OpenCode**, dan **Roo Code**).
|
|
36
|
+
4. ⚙️ Melakukan auto-konfigurasi jika mendeteksi editor **Cursor** (`.cursor/mcp.json`) atau **Antigravity** (`mcp_config.json`).
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
@@ -126,9 +126,15 @@ Installer `install.sh` sebelumnya mengkloning repositori dan menginstal Node.js
|
|
|
126
126
|
rm -rf ~/.nvstudio-mcp
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
### 4. Hapus Konfigurasi dari Editor AI Anda
|
|
129
|
+
### 4. Hapus Konfigurasi dan Skills dari Editor AI Anda
|
|
130
130
|
Jika Anda memasukkan konfigurasi MCP secara otomatis ke Cursor atau secara manual ke Claude Desktop, buka file pengaturan `mcp.json` tersebut dan **hapus blok `"nvstudio-mcp"`** dari dalam objek `mcpServers`.
|
|
131
131
|
|
|
132
|
+
**Untuk membersihkan skill bawaan (Universal Skill Installer):**
|
|
133
|
+
Anda bisa menghapus manual symlink/folder shortcut (yang bernama awalan `roblox-`) dari dalam folder konfigurasi agen Anda:
|
|
134
|
+
- **Cursor / Workspace Lokal**: Hapus isi `.cursor/skills` dan `.cursor/roblox-brain.md`
|
|
135
|
+
- **Claude Code**: Hapus dari `~/.claude/skills`
|
|
136
|
+
- **Codex / OpenCode**: Hapus dari direktori lokal atau global (`~/.codex/skills`, `~/.opencode/skills`).
|
|
137
|
+
|
|
132
138
|
---
|
|
133
139
|
|
|
134
140
|
**Selamat Menciptakan Game Impian Anda dengan AI! 🎮🤖**
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
|
66
66
|
});
|
|
67
67
|
// 5. Handler untuk menangani pemanggilan call_mcp_tool
|
|
68
68
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
69
|
+
await roblox_bridge_1.bridgeReadyPromise;
|
|
69
70
|
if (request.params.name !== "call_mcp_tool") {
|
|
70
71
|
throw new Error(`Tool tidak dikenali: ${request.params.name}`);
|
|
71
72
|
}
|
|
@@ -140,6 +141,27 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
140
141
|
let result;
|
|
141
142
|
if (roblox_bridge_1.isBridgeHosting) {
|
|
142
143
|
// Instance ini sendiri yang meng-host bridge server
|
|
144
|
+
if (!(0, roblox_bridge_1.isStudioConnected)()) {
|
|
145
|
+
for (let i = 0; i < 6; i++) {
|
|
146
|
+
await new Promise(r => setTimeout(r, 50));
|
|
147
|
+
if ((0, roblox_bridge_1.isStudioConnected)())
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
if (!(0, roblox_bridge_1.isStudioConnected)()) {
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: JSON.stringify({
|
|
156
|
+
status: "error",
|
|
157
|
+
error: "Roblox Studio tidak terdeteksi aktif. Pastikan Roblox Studio dibuka, place telah dimuat, dan plugin nvstudio-mcp dalam status terhubung.",
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
isError: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
143
165
|
result = await (0, task_queue_1.addTaskToQueue)(command, target, data);
|
|
144
166
|
}
|
|
145
167
|
else {
|
package/dist/roblox-bridge.js
CHANGED
|
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.isBridgeHosting = void 0;
|
|
6
|
+
exports.bridgeReadyPromise = exports.isBridgeHosting = void 0;
|
|
7
|
+
exports.isStudioConnected = isStudioConnected;
|
|
7
8
|
exports.setMcpConnected = setMcpConnected;
|
|
8
9
|
exports.startBridgeServer = startBridgeServer;
|
|
9
10
|
const express_1 = __importDefault(require("express"));
|
|
@@ -14,6 +15,10 @@ const child_process_1 = require("child_process");
|
|
|
14
15
|
const task_queue_1 = require("./task-queue");
|
|
15
16
|
const { version: SERVER_VERSION } = require('../package.json');
|
|
16
17
|
exports.isBridgeHosting = false;
|
|
18
|
+
let resolveBridgeReady;
|
|
19
|
+
exports.bridgeReadyPromise = new Promise((resolve) => {
|
|
20
|
+
resolveBridgeReady = resolve;
|
|
21
|
+
});
|
|
17
22
|
// Global process error handlers agar server bridge tidak mati mendadak
|
|
18
23
|
process.on('uncaughtException', (err) => {
|
|
19
24
|
console.error('[Bridge] Uncaught Exception:', err);
|
|
@@ -29,15 +34,58 @@ const sessionStart = new Date().toISOString();
|
|
|
29
34
|
// Per-studio pending events queue: events yang belum diambil oleh Studio
|
|
30
35
|
const studioEventQueues = new Map();
|
|
31
36
|
const studioLastSeen = new Map();
|
|
37
|
+
const activeStudioPollers = new Map();
|
|
32
38
|
setInterval(() => {
|
|
33
39
|
const now = Date.now();
|
|
40
|
+
let changed = false;
|
|
34
41
|
for (const [sid, lastSeen] of studioLastSeen.entries()) {
|
|
35
|
-
if (now - lastSeen >
|
|
42
|
+
if (now - lastSeen > 35000) {
|
|
36
43
|
studioEventQueues.delete(sid);
|
|
37
44
|
studioLastSeen.delete(sid);
|
|
45
|
+
const poller = activeStudioPollers.get(sid);
|
|
46
|
+
if (poller) {
|
|
47
|
+
clearTimeout(poller.timer);
|
|
48
|
+
try {
|
|
49
|
+
poller.res.end();
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
activeStudioPollers.delete(sid);
|
|
53
|
+
}
|
|
54
|
+
changed = true;
|
|
38
55
|
}
|
|
39
56
|
}
|
|
40
|
-
|
|
57
|
+
if (changed) {
|
|
58
|
+
broadcastStatus();
|
|
59
|
+
}
|
|
60
|
+
}, 5000);
|
|
61
|
+
function isStudioConnected() {
|
|
62
|
+
return studioEventQueues.size > 0 || activeStudioPollers.size > 0;
|
|
63
|
+
}
|
|
64
|
+
function flushEventsToStudio(sid) {
|
|
65
|
+
const poller = activeStudioPollers.get(sid);
|
|
66
|
+
if (!poller)
|
|
67
|
+
return false;
|
|
68
|
+
const queue = studioEventQueues.get(sid) ?? [];
|
|
69
|
+
if (queue.length === 0)
|
|
70
|
+
return false;
|
|
71
|
+
studioEventQueues.set(sid, []); // kosongkan antrian
|
|
72
|
+
clearTimeout(poller.timer);
|
|
73
|
+
activeStudioPollers.delete(sid);
|
|
74
|
+
const events = [
|
|
75
|
+
{ kind: 'status', mcpConnected, studioCount: Math.max(studioEventQueues.size, 1), serverVersion: SERVER_VERSION },
|
|
76
|
+
...queue
|
|
77
|
+
];
|
|
78
|
+
try {
|
|
79
|
+
poller.res.setHeader('Content-Type', 'text/plain');
|
|
80
|
+
poller.res.setHeader('Cache-Control', 'no-cache, no-store');
|
|
81
|
+
const body = events.map(ev => `data: ${JSON.stringify(ev)}`).join('\n');
|
|
82
|
+
poller.res.send(body);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
41
89
|
function enqueueForStudio(studioId, event) {
|
|
42
90
|
if (studioId) {
|
|
43
91
|
// Event untuk studio tertentu
|
|
@@ -47,6 +95,7 @@ function enqueueForStudio(studioId, event) {
|
|
|
47
95
|
q.push(event);
|
|
48
96
|
if (q.length > 100)
|
|
49
97
|
q.shift();
|
|
98
|
+
flushEventsToStudio(studioId);
|
|
50
99
|
}
|
|
51
100
|
else {
|
|
52
101
|
// Broadcast ke semua studio yang terdaftar
|
|
@@ -54,6 +103,7 @@ function enqueueForStudio(studioId, event) {
|
|
|
54
103
|
q.push(event);
|
|
55
104
|
if (q.length > 100)
|
|
56
105
|
q.shift();
|
|
106
|
+
flushEventsToStudio(sid);
|
|
57
107
|
}
|
|
58
108
|
}
|
|
59
109
|
}
|
|
@@ -92,7 +142,7 @@ function sendEvent(peer, kind, payload) {
|
|
|
92
142
|
function broadcastStatus() {
|
|
93
143
|
const statusEvent = {
|
|
94
144
|
mcpConnected,
|
|
95
|
-
studioCount:
|
|
145
|
+
studioCount: studioEventQueues.size,
|
|
96
146
|
serverVersion: SERVER_VERSION,
|
|
97
147
|
};
|
|
98
148
|
broadcastEvent('status', statusEvent);
|
|
@@ -179,12 +229,12 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
179
229
|
io.on('connection', (socket) => {
|
|
180
230
|
socket.emit('init', {
|
|
181
231
|
logs: activityLogs,
|
|
182
|
-
stats: { totalTasks, successCount, errorCount, sessionStart, studioConnected:
|
|
232
|
+
stats: { totalTasks, successCount, errorCount, sessionStart, studioConnected: studioEventQueues.size > 0, port },
|
|
183
233
|
});
|
|
184
234
|
});
|
|
185
235
|
// ── /api/stats ─────────────────────────────────────────────────────────────
|
|
186
236
|
app.get('/api/stats', (req, res) => {
|
|
187
|
-
const activeStudios =
|
|
237
|
+
const activeStudios = studioEventQueues.size;
|
|
188
238
|
res.json({
|
|
189
239
|
totalTasks, successCount, errorCount, sessionStart,
|
|
190
240
|
studioConnected: activeStudios > 0,
|
|
@@ -196,7 +246,7 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
196
246
|
// ── /api/ping ─────────────────────────────────────────────────────────────
|
|
197
247
|
app.get('/api/ping', (req, res) => {
|
|
198
248
|
const uptime = Math.floor((Date.now() - new Date(sessionStart).getTime()) / 1000);
|
|
199
|
-
const activeStudios =
|
|
249
|
+
const activeStudios = studioEventQueues.size;
|
|
200
250
|
res.json({
|
|
201
251
|
status: 'ok',
|
|
202
252
|
version: SERVER_VERSION,
|
|
@@ -205,18 +255,18 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
205
255
|
activeStudios: activeStudios
|
|
206
256
|
});
|
|
207
257
|
});
|
|
208
|
-
// ── /api/stream (
|
|
209
|
-
// Roblox HttpService:GetAsync()
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
258
|
+
// ── /api/stream (True Long-Polling endpoint untuk Roblox GetAsync) ─────
|
|
259
|
+
// Roblox HttpService:GetAsync() menahan koneksi sampai server merespons.
|
|
260
|
+
// Jika ada task yang menunggu: kirim seketika (<1ms).
|
|
261
|
+
// Jika tidak ada task: tahan koneksi (hanging GET) hingga task baru tiba
|
|
262
|
+
// atau timeout 15 detik (heartbeat) untuk menjaga koneksi tetap segar.
|
|
213
263
|
app.get('/api/stream', (req, res) => {
|
|
214
264
|
const studioId = req.query.studioId || `anon-${Date.now()}`;
|
|
215
265
|
studioLastSeen.set(studioId, Date.now());
|
|
216
266
|
// Daftarkan studio jika belum ada
|
|
217
267
|
if (!studioEventQueues.has(studioId)) {
|
|
218
268
|
studioEventQueues.set(studioId, []);
|
|
219
|
-
io.emit('studio-status', { connected: true, studioCount: studioEventQueues.size, studioId });
|
|
269
|
+
io.emit('studio-status', { connected: true, studioCount: Math.max(studioEventQueues.size, 1), studioId });
|
|
220
270
|
addLog(io, 'system', `Studio terdaftar. (ID: ${studioId.substring(0, 8)}…, Total: ${studioEventQueues.size})`);
|
|
221
271
|
if (!isDaemon && !dashboardOpened) {
|
|
222
272
|
dashboardOpened = true;
|
|
@@ -228,26 +278,55 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
228
278
|
});
|
|
229
279
|
}
|
|
230
280
|
}
|
|
281
|
+
// Jika poller lama untuk studio ini masih aktif, selesaikan dulu
|
|
282
|
+
const oldPoller = activeStudioPollers.get(studioId);
|
|
283
|
+
if (oldPoller) {
|
|
284
|
+
clearTimeout(oldPoller.timer);
|
|
285
|
+
try {
|
|
286
|
+
oldPoller.res.end();
|
|
287
|
+
}
|
|
288
|
+
catch { }
|
|
289
|
+
activeStudioPollers.delete(studioId);
|
|
290
|
+
}
|
|
231
291
|
// Kumpulkan semua events yang belum dikirim ke studio ini
|
|
232
292
|
const queue = studioEventQueues.get(studioId) ?? [];
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
293
|
+
if (queue.length > 0) {
|
|
294
|
+
studioEventQueues.set(studioId, []); // kosongkan antrian
|
|
295
|
+
const events = [
|
|
296
|
+
{ kind: 'status', mcpConnected, studioCount: studioEventQueues.size, serverVersion: SERVER_VERSION },
|
|
297
|
+
];
|
|
298
|
+
for (const ev of queue) {
|
|
299
|
+
if (ev.kind !== 'status')
|
|
300
|
+
events.push(ev);
|
|
301
|
+
}
|
|
302
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
303
|
+
res.setHeader('Cache-Control', 'no-cache, no-store');
|
|
304
|
+
const body = events.map(ev => `data: ${JSON.stringify(ev)}`).join('\n');
|
|
305
|
+
return res.send(body);
|
|
243
306
|
}
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
307
|
+
// Jika antrean kosong, TAHAN koneksi (Long-Polling) maksimal 15 detik
|
|
308
|
+
const timer = setTimeout(() => {
|
|
309
|
+
activeStudioPollers.delete(studioId);
|
|
310
|
+
try {
|
|
311
|
+
const events = [
|
|
312
|
+
{ kind: 'heartbeat', timestamp: Date.now() },
|
|
313
|
+
{ kind: 'status', mcpConnected, studioCount: studioEventQueues.size, serverVersion: SERVER_VERSION }
|
|
314
|
+
];
|
|
315
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
316
|
+
res.setHeader('Cache-Control', 'no-cache, no-store');
|
|
317
|
+
const body = events.map(ev => `data: ${JSON.stringify(ev)}`).join('\n');
|
|
318
|
+
res.send(body);
|
|
319
|
+
}
|
|
320
|
+
catch { }
|
|
321
|
+
}, 15000);
|
|
322
|
+
activeStudioPollers.set(studioId, { res, timer });
|
|
323
|
+
req.on('close', () => {
|
|
324
|
+
const current = activeStudioPollers.get(studioId);
|
|
325
|
+
if (current && current.res === res) {
|
|
326
|
+
clearTimeout(current.timer);
|
|
327
|
+
activeStudioPollers.delete(studioId);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
251
330
|
});
|
|
252
331
|
// ── /api/events (SSE persisten untuk browser/klien modern) ───────────────
|
|
253
332
|
app.get('/api/events', (req, res) => {
|
|
@@ -329,6 +408,20 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
329
408
|
// ── /api/tasks/enqueue (dari proses MCP eksternal / bridge forwarding) ────
|
|
330
409
|
app.post('/api/tasks/enqueue', async (req, res) => {
|
|
331
410
|
const { command, target, data } = req.body;
|
|
411
|
+
// Fast-check: pastikan setidaknya 1 Studio aktif polling
|
|
412
|
+
if (!isStudioConnected()) {
|
|
413
|
+
for (let i = 0; i < 6; i++) {
|
|
414
|
+
await new Promise(r => setTimeout(r, 50));
|
|
415
|
+
if (isStudioConnected())
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
if (!isStudioConnected()) {
|
|
419
|
+
return res.json({
|
|
420
|
+
status: 'error',
|
|
421
|
+
error: 'Roblox Studio tidak terdeteksi aktif. Pastikan Roblox Studio dibuka, place telah dimuat, dan plugin nvstudio-mcp dalam status terhubung.'
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
332
425
|
try {
|
|
333
426
|
const result = await (0, task_queue_1.addTaskToQueue)(command, target, data);
|
|
334
427
|
res.json({ status: 'success', result });
|
|
@@ -374,6 +467,7 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
374
467
|
const server = httpServer.listen(port, () => {
|
|
375
468
|
exports.isBridgeHosting = true;
|
|
376
469
|
console.error(`[Bridge] SSE Bridge Server aktif di http://localhost:${port}`);
|
|
470
|
+
resolveBridgeReady(true);
|
|
377
471
|
});
|
|
378
472
|
server.on('error', async (err) => {
|
|
379
473
|
if (err.code === 'EADDRINUSE') {
|
|
@@ -385,6 +479,7 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
385
479
|
if (pingRes.ok) {
|
|
386
480
|
exports.isBridgeHosting = false;
|
|
387
481
|
console.error(`[Bridge] Port ${port} sudah digunakan oleh NVStudio Bridge aktif. Menggunakan instance yang sudah berjalan.`);
|
|
482
|
+
resolveBridgeReady(false);
|
|
388
483
|
return;
|
|
389
484
|
}
|
|
390
485
|
}
|
|
@@ -392,14 +487,16 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
392
487
|
if (retriesLeft > 0) {
|
|
393
488
|
console.error(`[Bridge] Port ${port} zombie. Mencoba membunuh...`);
|
|
394
489
|
killZombieOnPort(port);
|
|
395
|
-
setTimeout(() => tryListen(retriesLeft - 1),
|
|
490
|
+
setTimeout(() => tryListen(retriesLeft - 1), 500);
|
|
396
491
|
}
|
|
397
492
|
else {
|
|
398
493
|
console.error(`[Bridge] FATAL: Port ${port} tidak dapat dibuka.`);
|
|
494
|
+
resolveBridgeReady(false);
|
|
399
495
|
}
|
|
400
496
|
}
|
|
401
497
|
else {
|
|
402
498
|
console.error(`[Bridge] FATAL: Gagal mendengarkan port ${port}:`, err.message);
|
|
499
|
+
resolveBridgeReady(false);
|
|
403
500
|
process.exit(1);
|
|
404
501
|
}
|
|
405
502
|
});
|
package/package.json
CHANGED
|
@@ -335,7 +335,7 @@ function startConnectionLoop()
|
|
|
335
335
|
handleSSELine(line)
|
|
336
336
|
end
|
|
337
337
|
end
|
|
338
|
-
task.wait(0.
|
|
338
|
+
task.wait(0.05) -- Instant loop: server menahan request (hanging GET) saat idle
|
|
339
339
|
else
|
|
340
340
|
consecutiveErrors = consecutiveErrors + 1
|
|
341
341
|
-- Toleransi 2x gagal sebelum menandai server unreachable
|
|
@@ -824,7 +824,7 @@ function startConnectionLoop()
|
|
|
824
824
|
handleSSELine(line)
|
|
825
825
|
end
|
|
826
826
|
end
|
|
827
|
-
task.wait(0.
|
|
827
|
+
task.wait(0.05) -- Instant loop: server menahan request (hanging GET) saat idle
|
|
828
828
|
else
|
|
829
829
|
consecutiveErrors = consecutiveErrors + 1
|
|
830
830
|
-- Toleransi 2x gagal sebelum menandai server unreachable
|
|
@@ -1088,7 +1088,8 @@ end
|
|
|
1088
1088
|
if targetInstance and targetInstance ~= game then
|
|
1089
1089
|
local descendants = {}
|
|
1090
1090
|
pcall(function() descendants = targetInstance:GetDescendants() end)
|
|
1091
|
-
for
|
|
1091
|
+
for i, desc in ipairs(descendants) do
|
|
1092
|
+
if i % 300 == 0 then task.wait() end
|
|
1092
1093
|
if matchCount >= MAX_MATCHES then break end
|
|
1093
1094
|
if desc:IsA("LuaSourceContainer") then
|
|
1094
1095
|
if searchInScript(desc) then
|
|
@@ -1100,7 +1101,8 @@ end
|
|
|
1100
1101
|
for _, service in ipairs(searchableServices) do
|
|
1101
1102
|
local descendants = {}
|
|
1102
1103
|
pcall(function() descendants = service:GetDescendants() end)
|
|
1103
|
-
for
|
|
1104
|
+
for i, desc in ipairs(descendants) do
|
|
1105
|
+
if i % 300 == 0 then task.wait() end
|
|
1104
1106
|
if matchCount >= MAX_MATCHES then break end
|
|
1105
1107
|
if desc:IsA("LuaSourceContainer") then
|
|
1106
1108
|
if searchInScript(desc) then
|
|
@@ -48,7 +48,8 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
48
48
|
if targetInstance and targetInstance ~= game then
|
|
49
49
|
local descendants = {}
|
|
50
50
|
pcall(function() descendants = targetInstance:GetDescendants() end)
|
|
51
|
-
for
|
|
51
|
+
for i, desc in ipairs(descendants) do
|
|
52
|
+
if i % 300 == 0 then task.wait() end
|
|
52
53
|
if matchCount >= MAX_MATCHES then break end
|
|
53
54
|
if desc:IsA("LuaSourceContainer") then
|
|
54
55
|
if searchInScript(desc) then
|
|
@@ -60,7 +61,8 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
60
61
|
for _, service in ipairs(searchableServices) do
|
|
61
62
|
local descendants = {}
|
|
62
63
|
pcall(function() descendants = service:GetDescendants() end)
|
|
63
|
-
for
|
|
64
|
+
for i, desc in ipairs(descendants) do
|
|
65
|
+
if i % 300 == 0 then task.wait() end
|
|
64
66
|
if matchCount >= MAX_MATCHES then break end
|
|
65
67
|
if desc:IsA("LuaSourceContainer") then
|
|
66
68
|
if searchInScript(desc) then
|