@chrrxs/robloxstudio-mcp 2.23.1 → 3.0.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.
Files changed (50) hide show
  1. package/dist/index.js +1688 -4547
  2. package/package.json +7 -5
  3. package/studio-plugin/MCPPlugin.rbxmx +489 -1965
  4. package/studio-plugin/.Carbon.rbxm.lock +0 -0
  5. package/studio-plugin/Carbon.rbxm +0 -0
  6. package/studio-plugin/INSTALLATION.md +0 -170
  7. package/studio-plugin/MCPInspectorPlugin.rbxmx +0 -171527
  8. package/studio-plugin/default.project.json +0 -19
  9. package/studio-plugin/dev.project.json +0 -23
  10. package/studio-plugin/include/LibMP.lua +0 -156378
  11. package/studio-plugin/inspector-icon.png +0 -0
  12. package/studio-plugin/package-lock.json +0 -706
  13. package/studio-plugin/package.json +0 -19
  14. package/studio-plugin/plugin.json +0 -10
  15. package/studio-plugin/src/modules/AssetSanitizationPolicy.ts +0 -127
  16. package/studio-plugin/src/modules/ClientBroker.ts +0 -450
  17. package/studio-plugin/src/modules/Communication.ts +0 -632
  18. package/studio-plugin/src/modules/EvalBridges.ts +0 -255
  19. package/studio-plugin/src/modules/HttpDiagnostics.ts +0 -50
  20. package/studio-plugin/src/modules/LuauExec.ts +0 -403
  21. package/studio-plugin/src/modules/Recording.ts +0 -28
  22. package/studio-plugin/src/modules/RenderMonitor.ts +0 -60
  23. package/studio-plugin/src/modules/RuntimeLogBuffer.ts +0 -210
  24. package/studio-plugin/src/modules/ServerUrlSettings.ts +0 -117
  25. package/studio-plugin/src/modules/State.ts +0 -39
  26. package/studio-plugin/src/modules/StopPlayMonitor.ts +0 -267
  27. package/studio-plugin/src/modules/UI.ts +0 -597
  28. package/studio-plugin/src/modules/Utils.ts +0 -469
  29. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +0 -391
  30. package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +0 -460
  31. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +0 -481
  32. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +0 -170
  33. package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +0 -149
  34. package/studio-plugin/src/modules/handlers/GenerateModelHandlers.ts +0 -168
  35. package/studio-plugin/src/modules/handlers/InputHandlers.ts +0 -163
  36. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +0 -380
  37. package/studio-plugin/src/modules/handlers/LogHandlers.ts +0 -14
  38. package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +0 -44
  39. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +0 -354
  40. package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +0 -1263
  41. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +0 -191
  42. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -874
  43. package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +0 -216
  44. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +0 -530
  45. package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +0 -386
  46. package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +0 -172
  47. package/studio-plugin/src/modules/handlers/TestHandlers.ts +0 -350
  48. package/studio-plugin/src/server/index.server.ts +0 -135
  49. package/studio-plugin/src/types/index.d.ts +0 -57
  50. package/studio-plugin/tsconfig.json +0 -20
@@ -1,874 +0,0 @@
1
- import Utils from "../Utils";
2
-
3
- const { getInstancePath, getInstanceByPath, readScriptSource } = Utils;
4
-
5
- interface TreeNode {
6
- name: string;
7
- className: string;
8
- path?: string;
9
- children: TreeNode[];
10
- hasSource?: boolean;
11
- scriptType?: string;
12
- enabled?: boolean;
13
- }
14
-
15
- function getFileTree(requestData: Record<string, unknown>) {
16
- const path = (requestData.path as string) ?? "";
17
- const startInstance = getInstanceByPath(path);
18
-
19
- if (!startInstance) {
20
- return { error: `Path not found: ${path}` };
21
- }
22
-
23
- function buildTree(instance: Instance, depth: number): TreeNode {
24
- if (depth > 10) {
25
- return { name: instance.Name, className: instance.ClassName, children: [] };
26
- }
27
-
28
- const node: TreeNode = {
29
- name: instance.Name,
30
- className: instance.ClassName,
31
- path: getInstancePath(instance),
32
- children: [],
33
- };
34
-
35
- if (instance.IsA("LuaSourceContainer")) {
36
- node.hasSource = true;
37
- node.scriptType = instance.ClassName;
38
- if (instance.IsA("BaseScript")) {
39
- node.enabled = instance.Enabled;
40
- }
41
- }
42
-
43
- for (const child of instance.GetChildren()) {
44
- node.children.push(buildTree(child, depth + 1));
45
- }
46
-
47
- return node;
48
- }
49
-
50
- return {
51
- tree: buildTree(startInstance, 0),
52
- timestamp: tick(),
53
- };
54
- }
55
-
56
- function searchFiles(requestData: Record<string, unknown>) {
57
- const query = requestData.query as string;
58
- const searchType = (requestData.searchType as string) ?? "name";
59
-
60
- if (!query) return { error: "Query is required" };
61
-
62
- const results: { name: string; className: string; path: string; hasSource: boolean; enabled?: boolean }[] = [];
63
-
64
- function searchRecursive(instance: Instance) {
65
- let match = false;
66
-
67
- if (searchType === "name") {
68
- match = instance.Name.lower().find(query.lower())[0] !== undefined;
69
- } else if (searchType === "type") {
70
- match = instance.ClassName.lower().find(query.lower())[0] !== undefined;
71
- } else if (searchType === "content" && instance.IsA("LuaSourceContainer")) {
72
- match = readScriptSource(instance).lower().find(query.lower())[0] !== undefined;
73
- }
74
-
75
- if (match) {
76
- const entry: { name: string; className: string; path: string; hasSource: boolean; enabled?: boolean } = {
77
- name: instance.Name,
78
- className: instance.ClassName,
79
- path: getInstancePath(instance),
80
- hasSource: instance.IsA("LuaSourceContainer"),
81
- };
82
- if (instance.IsA("BaseScript")) {
83
- entry.enabled = instance.Enabled;
84
- }
85
- results.push(entry);
86
- }
87
-
88
- for (const child of instance.GetChildren()) {
89
- searchRecursive(child);
90
- }
91
- }
92
-
93
- searchRecursive(game);
94
-
95
- return { results, query, searchType, count: results.size() };
96
- }
97
-
98
- function getPlaceInfo(_requestData: Record<string, unknown>) {
99
- const dataModelName = game.Name;
100
- let placeName = dataModelName;
101
-
102
- if (game.PlaceId > 0) {
103
- const MarketplaceService = game.GetService("MarketplaceService");
104
- const [ok, info] = pcall(() => MarketplaceService.GetProductInfo(game.PlaceId));
105
- if (ok && info !== undefined) {
106
- const name = (info as { Name?: string }).Name;
107
- if (typeIs(name, "string") && name !== "") {
108
- placeName = name;
109
- }
110
- }
111
- }
112
-
113
- return {
114
- placeName,
115
- dataModelName,
116
- placeId: game.PlaceId,
117
- gameId: game.GameId,
118
- jobId: game.JobId,
119
- workspace: {
120
- name: game.Workspace.Name,
121
- className: game.Workspace.ClassName,
122
- },
123
- };
124
- }
125
-
126
- function getServices(requestData: Record<string, unknown>) {
127
- const serviceName = requestData.serviceName as string | undefined;
128
-
129
- if (serviceName) {
130
- const [ok, service] = pcall(() => game.GetService(serviceName as keyof Services));
131
- if (ok && service) {
132
- return {
133
- service: {
134
- name: service.Name,
135
- className: service.ClassName,
136
- path: getInstancePath(service as Instance),
137
- childCount: (service as Instance).GetChildren().size(),
138
- },
139
- };
140
- } else {
141
- return { error: `Service not found: ${serviceName}` };
142
- }
143
- } else {
144
- const services: { name: string; className: string; path: string; childCount: number }[] = [];
145
- const commonServices = [
146
- "Workspace", "Players", "StarterGui", "StarterPack", "StarterPlayer",
147
- "ReplicatedStorage", "ServerStorage", "ServerScriptService",
148
- "HttpService", "TeleportService", "DataStoreService",
149
- ];
150
-
151
- for (const svcName of commonServices) {
152
- const [ok, service] = pcall(() => game.GetService(svcName as keyof Services));
153
- if (ok && service) {
154
- services.push({
155
- name: service.Name,
156
- className: service.ClassName,
157
- path: getInstancePath(service as Instance),
158
- childCount: (service as Instance).GetChildren().size(),
159
- });
160
- }
161
- }
162
-
163
- return { services };
164
- }
165
- }
166
-
167
- function searchObjects(requestData: Record<string, unknown>) {
168
- const query = requestData.query as string;
169
- const searchType = (requestData.searchType as string) ?? "name";
170
- const propertyName = requestData.propertyName as string | undefined;
171
-
172
- if (!query) return { error: "Query is required" };
173
-
174
- const results: { name: string; className: string; path: string }[] = [];
175
-
176
- function searchRecursive(instance: Instance) {
177
- let match = false;
178
-
179
- if (searchType === "name") {
180
- match = instance.Name.lower().find(query.lower())[0] !== undefined;
181
- } else if (searchType === "class") {
182
- match = instance.ClassName.lower().find(query.lower())[0] !== undefined;
183
- } else if (searchType === "property" && propertyName) {
184
- const [success, value] = pcall(() => tostring((instance as unknown as Record<string, unknown>)[propertyName]));
185
- if (success) {
186
- match = (value as string).lower().find(query.lower())[0] !== undefined;
187
- }
188
- }
189
-
190
- if (match) {
191
- results.push({
192
- name: instance.Name,
193
- className: instance.ClassName,
194
- path: getInstancePath(instance),
195
- });
196
- }
197
-
198
- for (const child of instance.GetChildren()) {
199
- searchRecursive(child);
200
- }
201
- }
202
-
203
- searchRecursive(game);
204
-
205
- return { results, query, searchType, count: results.size() };
206
- }
207
-
208
- function getInstanceProperties(requestData: Record<string, unknown>) {
209
- const instancePath = requestData.instancePath as string;
210
- const excludeSource = (requestData.excludeSource as boolean) ?? false;
211
- if (!instancePath) return { error: "Instance path is required" };
212
-
213
- const instance = getInstanceByPath(instancePath);
214
- if (!instance) return { error: `Instance not found: ${instancePath}` };
215
-
216
- const properties: Record<string, unknown> = {};
217
- const [success, result] = pcall(() => {
218
- const basicProps = ["Name", "ClassName", "Parent"];
219
- for (const prop of basicProps) {
220
- const [propSuccess, propValue] = pcall(() => {
221
- const val = (instance as unknown as Record<string, unknown>)[prop];
222
- if (prop === "Parent" && val) return getInstancePath(val as Instance);
223
- if (val === undefined) return "nil";
224
- return tostring(val);
225
- });
226
- if (propSuccess) properties[prop] = propValue;
227
- }
228
-
229
- const commonProps = [
230
- "Size", "Position", "Rotation", "CFrame", "Anchored", "CanCollide",
231
- "Transparency", "BrickColor", "Material", "Color", "Text", "TextColor3",
232
- "BackgroundColor3", "Image", "ImageColor3", "Visible", "Active", "ZIndex",
233
- "BorderSizePixel", "BackgroundTransparency", "ImageTransparency",
234
- "TextTransparency", "Value", "Enabled", "Brightness", "Range", "Shadows",
235
- "Face", "SurfaceType",
236
- ];
237
-
238
- for (const prop of commonProps) {
239
- const [propSuccess, propValue] = pcall(() => {
240
- const val = (instance as unknown as Record<string, unknown>)[prop];
241
- if (typeOf(val) === "UDim2") {
242
- const udim = val as UDim2;
243
- return {
244
- X: { Scale: udim.X.Scale, Offset: udim.X.Offset },
245
- Y: { Scale: udim.Y.Scale, Offset: udim.Y.Offset },
246
- _type: "UDim2",
247
- };
248
- }
249
- return tostring(val);
250
- });
251
- if (propSuccess) properties[prop] = propValue;
252
- }
253
-
254
- if (instance.IsA("LuaSourceContainer")) {
255
- if (!excludeSource) {
256
- properties.Source = readScriptSource(instance);
257
- } else {
258
- const src = readScriptSource(instance);
259
- properties.SourceLength = src.size();
260
- properties.LineCount = Utils.splitLines(src)[0].size();
261
- }
262
- if (instance.IsA("BaseScript")) {
263
- properties.Enabled = tostring(instance.Enabled);
264
- }
265
- }
266
-
267
- if (instance.IsA("Part")) {
268
- properties.Shape = tostring(instance.Shape);
269
- }
270
-
271
- if (instance.IsA("BasePart")) {
272
- properties.TopSurface = tostring(instance.TopSurface);
273
- properties.BottomSurface = tostring(instance.BottomSurface);
274
- }
275
-
276
- if (instance.IsA("MeshPart")) {
277
- properties.MeshId = tostring(instance.MeshId);
278
- properties.TextureID = tostring(instance.TextureID);
279
- }
280
-
281
- if (instance.IsA("SpecialMesh")) {
282
- properties.MeshId = tostring(instance.MeshId);
283
- properties.TextureId = tostring(instance.TextureId);
284
- properties.MeshType = tostring(instance.MeshType);
285
- }
286
-
287
- if (instance.IsA("Sound")) {
288
- properties.SoundId = tostring(instance.SoundId);
289
- properties.TimeLength = tostring(instance.TimeLength);
290
- properties.IsPlaying = tostring(instance.IsPlaying);
291
- }
292
-
293
- if (instance.IsA("Animation")) {
294
- properties.AnimationId = tostring(instance.AnimationId);
295
- }
296
-
297
- if (instance.IsA("Decal") || instance.IsA("Texture")) {
298
- properties.Texture = tostring((instance as Decal | Texture).Texture);
299
- }
300
-
301
- if (instance.IsA("Shirt")) {
302
- properties.ShirtTemplate = tostring(instance.ShirtTemplate);
303
- } else if (instance.IsA("Pants")) {
304
- properties.PantsTemplate = tostring(instance.PantsTemplate);
305
- } else if (instance.IsA("ShirtGraphic")) {
306
- properties.Graphic = tostring(instance.Graphic);
307
- }
308
-
309
- properties.ChildCount = tostring(instance.GetChildren().size());
310
- });
311
-
312
- if (success) {
313
- return { instancePath, className: instance.ClassName, properties };
314
- } else {
315
- return { error: `Failed to get properties: ${result}` };
316
- }
317
- }
318
-
319
- function getInstanceChildren(requestData: Record<string, unknown>) {
320
- const instancePath = requestData.instancePath as string;
321
- if (!instancePath) return { error: "Instance path is required" };
322
-
323
- const instance = getInstanceByPath(instancePath);
324
- if (!instance) return { error: `Instance not found: ${instancePath}` };
325
-
326
- const children: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean }[] = [];
327
- for (const child of instance.GetChildren()) {
328
- const entry: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean } = {
329
- name: child.Name,
330
- className: child.ClassName,
331
- path: getInstancePath(child),
332
- hasChildren: child.GetChildren().size() > 0,
333
- hasSource: child.IsA("LuaSourceContainer"),
334
- };
335
- if (child.IsA("BaseScript")) {
336
- entry.enabled = child.Enabled;
337
- }
338
- children.push(entry);
339
- }
340
-
341
- return { instancePath, children, count: children.size() };
342
- }
343
-
344
- function searchByProperty(requestData: Record<string, unknown>) {
345
- const propertyName = requestData.propertyName as string;
346
- const propertyValue = requestData.propertyValue as string;
347
-
348
- if (!propertyName || !propertyValue) {
349
- return { error: "Property name and value are required" };
350
- }
351
-
352
- const results: { name: string; className: string; path: string; propertyValue: string }[] = [];
353
-
354
- function searchRecursive(instance: Instance) {
355
- const [success, value] = pcall(() => tostring((instance as unknown as Record<string, unknown>)[propertyName]));
356
- if (success && (value as string).lower().find(propertyValue.lower())[0] !== undefined) {
357
- results.push({
358
- name: instance.Name,
359
- className: instance.ClassName,
360
- path: getInstancePath(instance),
361
- propertyValue: value as string,
362
- });
363
- }
364
- for (const child of instance.GetChildren()) {
365
- searchRecursive(child);
366
- }
367
- }
368
-
369
- searchRecursive(game);
370
- return { propertyName, propertyValue, results, count: results.size() };
371
- }
372
-
373
- function getClassInfo(requestData: Record<string, unknown>) {
374
- const className = requestData.className as string;
375
- if (!className) return { error: "Class name is required" };
376
-
377
- let [success, tempInstance] = pcall(() => new Instance(className as keyof CreatableInstances));
378
- let isService = false;
379
-
380
- if (!success) {
381
- const [serviceSuccess, serviceInstance] = pcall(() =>
382
- game.GetService(className as keyof Services),
383
- );
384
- if (serviceSuccess && serviceInstance) {
385
- success = true;
386
- tempInstance = serviceInstance as unknown as Instance;
387
- isService = true;
388
- }
389
- }
390
-
391
- if (!success) return { error: `Invalid class name: ${className}` };
392
-
393
- const classInfo: {
394
- className: string;
395
- isService: boolean;
396
- properties: string[];
397
- methods: string[];
398
- events: string[];
399
- } = { className, isService, properties: [], methods: [], events: [] };
400
-
401
- const commonProps = [
402
- "Name", "ClassName", "Parent", "Size", "Position", "Rotation", "CFrame",
403
- "Anchored", "CanCollide", "Transparency", "BrickColor", "Material", "Color",
404
- "Text", "TextColor3", "BackgroundColor3", "Image", "ImageColor3", "Visible",
405
- "Active", "ZIndex", "BorderSizePixel", "BackgroundTransparency",
406
- "ImageTransparency", "TextTransparency", "Value", "Enabled", "Brightness",
407
- "Range", "Shadows",
408
- ];
409
-
410
- for (const prop of commonProps) {
411
- const [propSuccess] = pcall(() => (tempInstance as unknown as Record<string, unknown>)[prop]);
412
- if (propSuccess) classInfo.properties.push(prop);
413
- }
414
-
415
- const commonMethods = [
416
- "Destroy", "Clone", "FindFirstChild", "FindFirstChildOfClass",
417
- "GetChildren", "IsA", "IsAncestorOf", "IsDescendantOf", "WaitForChild",
418
- ];
419
-
420
- for (const method of commonMethods) {
421
- const [methodSuccess] = pcall(() => (tempInstance as unknown as Record<string, unknown>)[method]);
422
- if (methodSuccess) classInfo.methods.push(method);
423
- }
424
-
425
- if (!isService) {
426
- (tempInstance as Instance).Destroy();
427
- }
428
-
429
- return classInfo;
430
- }
431
-
432
- function getProjectStructure(requestData: Record<string, unknown>) {
433
- const startPath = (requestData.path as string) ?? "";
434
- const maxDepth = (requestData.maxDepth as number) ?? 3;
435
- const showScriptsOnly = (requestData.scriptsOnly as boolean) ?? false;
436
-
437
- if (startPath === "" || startPath === "game") {
438
- const services: Record<string, unknown>[] = [];
439
- const mainServices = [
440
- "Workspace", "ServerScriptService", "ServerStorage", "ReplicatedStorage",
441
- "StarterGui", "StarterPack", "StarterPlayer", "Players",
442
- ];
443
-
444
- for (const serviceName of mainServices) {
445
- const [svcOk, service] = pcall(() => game.GetService(serviceName as keyof Services));
446
- if (svcOk && service) {
447
- services.push({
448
- name: service.Name,
449
- className: service.ClassName,
450
- path: getInstancePath(service as Instance),
451
- childCount: (service as Instance).GetChildren().size(),
452
- hasChildren: (service as Instance).GetChildren().size() > 0,
453
- });
454
- }
455
- }
456
-
457
- return {
458
- type: "service_overview",
459
- services,
460
- timestamp: tick(),
461
- note: "Use path parameter to explore specific locations (e.g., 'game.ServerScriptService')",
462
- };
463
- }
464
-
465
- const startInstance = getInstanceByPath(startPath);
466
- if (!startInstance) return { error: `Path not found: ${startPath}` };
467
-
468
- function getStructure(instance: Instance, depth: number): Record<string, unknown> {
469
- if (depth > maxDepth) {
470
- return {
471
- name: instance.Name,
472
- className: instance.ClassName,
473
- path: getInstancePath(instance),
474
- childCount: instance.GetChildren().size(),
475
- hasMore: true,
476
- note: "Max depth reached - use this path to explore further",
477
- };
478
- }
479
-
480
- const node: Record<string, unknown> = {
481
- name: instance.Name,
482
- className: instance.ClassName,
483
- path: getInstancePath(instance),
484
- children: [] as Record<string, unknown>[],
485
- };
486
-
487
- if (instance.IsA("LuaSourceContainer")) {
488
- node.hasSource = true;
489
- node.scriptType = instance.ClassName;
490
- if (instance.IsA("BaseScript")) {
491
- node.enabled = instance.Enabled;
492
- }
493
- }
494
-
495
- if (instance.IsA("GuiObject")) {
496
- node.visible = instance.Visible;
497
- if (instance.IsA("Frame") || instance.IsA("ScreenGui")) {
498
- node.guiType = "container";
499
- } else if (instance.IsA("TextLabel") || instance.IsA("TextButton")) {
500
- node.guiType = "text";
501
- const textInst = instance as TextLabel | TextButton;
502
- if (textInst.Text !== "") node.text = textInst.Text;
503
- } else if (instance.IsA("ImageLabel") || instance.IsA("ImageButton")) {
504
- node.guiType = "image";
505
- }
506
- }
507
-
508
- let children = instance.GetChildren();
509
- if (showScriptsOnly) {
510
- children = children.filter(
511
- (child) => child.IsA("BaseScript") || child.IsA("Folder") || child.IsA("ModuleScript"),
512
- );
513
- }
514
-
515
- const nodeChildren = node.children as Record<string, unknown>[];
516
- const childCount = children.size();
517
- if (childCount > 20 && depth < maxDepth) {
518
- const classGroups = new Map<string, Instance[]>();
519
- for (const child of children) {
520
- const cn = child.ClassName;
521
- if (!classGroups.has(cn)) classGroups.set(cn, []);
522
- classGroups.get(cn)!.push(child);
523
- }
524
-
525
- const childSummary: Record<string, unknown>[] = [];
526
- classGroups.forEach((classChildren, cn) => {
527
- childSummary.push({
528
- className: cn,
529
- count: classChildren.size(),
530
- examples: [classChildren[0]?.Name, classChildren[1]?.Name],
531
- });
532
- });
533
- node.childSummary = childSummary;
534
-
535
- classGroups.forEach((classChildren, cn) => {
536
- const limit = math.min(3, classChildren.size());
537
- for (let i = 0; i < limit; i++) {
538
- nodeChildren.push(getStructure(classChildren[i], depth + 1));
539
- }
540
- if (classChildren.size() > 3) {
541
- nodeChildren.push({
542
- name: `... ${classChildren.size() - 3} more ${cn} objects`,
543
- className: "MoreIndicator",
544
- path: `${getInstancePath(instance)} [${cn} children]`,
545
- note: "Use specific path to explore these objects",
546
- });
547
- }
548
- });
549
- } else {
550
- for (const child of children) {
551
- nodeChildren.push(getStructure(child, depth + 1));
552
- }
553
- }
554
-
555
- return node;
556
- }
557
-
558
- const result = getStructure(startInstance, 0);
559
- result.requestedPath = startPath;
560
- result.maxDepth = maxDepth;
561
- result.scriptsOnly = showScriptsOnly;
562
- result.timestamp = tick();
563
-
564
- return result;
565
- }
566
-
567
- // Split a Lua pattern on TOP-LEVEL "|" into alternatives. Lua patterns have no
568
- // alternation operator, so "foo|bar" would otherwise be matched as the literal
569
- // text "foo|bar" and silently never hit. "%|" stays a literal pipe, and "%bxy"
570
- // keeps both balanced-match delimiter characters.
571
- function splitLuaAlternation(pattern: string): string[] {
572
- const parts: string[] = [];
573
- let current = "";
574
- let i = 1;
575
- const n = pattern.size();
576
- let inCharClass = false;
577
- while (i <= n) {
578
- const c = string.sub(pattern, i, i);
579
- if (c === "%") {
580
- if (string.sub(pattern, i + 1, i + 1) === "b") {
581
- current += string.sub(pattern, i, math.min(i + 3, n));
582
- i += 4;
583
- continue;
584
- }
585
- // Preserve an escape pair (e.g. %|, %., %d) intact.
586
- current += string.sub(pattern, i, i + 1);
587
- i += 2;
588
- } else if (c === "[") {
589
- inCharClass = true;
590
- current += c;
591
- i += 1;
592
- } else if (c === "]") {
593
- inCharClass = false;
594
- current += c;
595
- i += 1;
596
- } else if (c === "|" && !inCharClass) {
597
- parts.push(current);
598
- current = "";
599
- i += 1;
600
- } else {
601
- current += c;
602
- i += 1;
603
- }
604
- }
605
- parts.push(current);
606
- return parts;
607
- }
608
-
609
- // Return the earliest match across alternatives (mirrors regex alternation).
610
- function findFirstPattern(line: string, alternatives: string[]): [number | undefined, number | undefined] {
611
- let bestStart: number | undefined;
612
- let bestEnd: number | undefined;
613
- for (const alt of alternatives) {
614
- if (alt === "") continue;
615
- const [s, e] = string.find(line, alt);
616
- if (s !== undefined && (bestStart === undefined || s < bestStart)) {
617
- bestStart = s;
618
- bestEnd = e as number;
619
- }
620
- }
621
- return [bestStart, bestEnd];
622
- }
623
-
624
- function grepScripts(requestData: Record<string, unknown>) {
625
- const pattern = requestData.pattern as string;
626
- if (!pattern) return { error: "pattern is required" };
627
-
628
- const usePattern = (requestData.usePattern as boolean) ?? false;
629
- if (usePattern && requestData.caseSensitive === false) {
630
- return {
631
- error: "Case-insensitive Lua pattern search is not supported. Omit caseSensitive or pass caseSensitive: true with usePattern: true, or use literal search.",
632
- };
633
- }
634
-
635
- const caseSensitive = usePattern ? true : ((requestData.caseSensitive as boolean) ?? false);
636
- const contextLines = (requestData.contextLines as number) ?? 0;
637
- const maxResults = (requestData.maxResults as number) ?? 100;
638
- const maxResultsPerScript = (requestData.maxResultsPerScript as number) ?? 0;
639
- const filesOnly = (requestData.filesOnly as boolean) ?? false;
640
- const searchPath = (requestData.path as string) ?? "";
641
- const classFilter = requestData.classFilter as string | undefined;
642
-
643
- const startInstance = searchPath !== "" ? getInstanceByPath(searchPath) : game;
644
- if (!startInstance) return { error: `Path not found: ${searchPath}` };
645
-
646
- // Prepare pattern for matching
647
- const searchPattern = caseSensitive ? pattern : pattern.lower();
648
- // Pre-split top-level "|" alternation once (pattern mode only).
649
- const patternAlternatives = usePattern ? splitLuaAlternation(searchPattern) : undefined;
650
-
651
- interface LineMatch {
652
- line: number;
653
- column: number;
654
- text: string;
655
- before: string[];
656
- after: string[];
657
- }
658
-
659
- interface ScriptResult {
660
- instancePath: string;
661
- name: string;
662
- className: string;
663
- enabled?: boolean;
664
- matches: LineMatch[];
665
- }
666
-
667
- const results: ScriptResult[] = [];
668
- let totalMatches = 0;
669
- let scriptsSearched = 0;
670
- let hitLimit = false;
671
-
672
- function searchInstance(instance: Instance) {
673
- if (hitLimit) return;
674
-
675
- if (instance.IsA("LuaSourceContainer")) {
676
- // Apply class filter
677
- if (classFilter) {
678
- if (!instance.ClassName.lower().find(classFilter.lower())[0]) return;
679
- }
680
-
681
- scriptsSearched++;
682
- const source = readScriptSource(instance);
683
- const [lines] = Utils.splitLines(source);
684
- const scriptMatches: LineMatch[] = [];
685
- let scriptMatchCount = 0;
686
-
687
- for (let i = 0; i < lines.size(); i++) {
688
- if (hitLimit) break;
689
- if (maxResultsPerScript > 0 && scriptMatchCount >= maxResultsPerScript) break;
690
-
691
- const line = lines[i];
692
- const searchLine = caseSensitive ? line : line.lower();
693
-
694
- let matchStart: number | undefined;
695
- let matchEnd: number | undefined;
696
-
697
- if (usePattern) {
698
- [matchStart, matchEnd] = findFirstPattern(searchLine, patternAlternatives!);
699
- } else {
700
- [matchStart, matchEnd] = string.find(searchLine, searchPattern, 1, true);
701
- }
702
-
703
- if (matchStart !== undefined) {
704
- scriptMatchCount++;
705
- totalMatches++;
706
-
707
- if (totalMatches > maxResults) {
708
- hitLimit = true;
709
- break;
710
- }
711
-
712
- if (!filesOnly) {
713
- // Gather context lines
714
- const before: string[] = [];
715
- const after: string[] = [];
716
-
717
- if (contextLines > 0) {
718
- const beforeStart = math.max(0, i - contextLines);
719
- for (let j = beforeStart; j < i; j++) {
720
- before.push(lines[j]);
721
- }
722
- const afterEnd = math.min(lines.size() - 1, i + contextLines);
723
- for (let j = i + 1; j <= afterEnd; j++) {
724
- after.push(lines[j]);
725
- }
726
- }
727
-
728
- scriptMatches.push({
729
- line: i + 1, // 1-indexed
730
- column: matchStart,
731
- text: line,
732
- before,
733
- after,
734
- });
735
- }
736
- }
737
- }
738
-
739
- if (scriptMatchCount > 0) {
740
- const scriptResult: ScriptResult = {
741
- instancePath: getInstancePath(instance),
742
- name: instance.Name,
743
- className: instance.ClassName,
744
- matches: scriptMatches,
745
- };
746
- if (instance.IsA("BaseScript")) {
747
- scriptResult.enabled = instance.Enabled;
748
- }
749
- results.push(scriptResult);
750
- }
751
- }
752
-
753
- for (const child of instance.GetChildren()) {
754
- if (hitLimit) return;
755
- searchInstance(child);
756
- }
757
- }
758
-
759
- searchInstance(startInstance);
760
-
761
- return {
762
- results,
763
- pattern,
764
- totalMatches: hitLimit ? `>${maxResults}` : totalMatches,
765
- scriptsSearched,
766
- scriptsMatched: results.size(),
767
- truncated: hitLimit,
768
- options: { caseSensitive, contextLines, usePattern, filesOnly, maxResults, maxResultsPerScript },
769
- };
770
- }
771
-
772
- function getDescendants(requestData: Record<string, unknown>) {
773
- const instancePath = requestData.instancePath as string;
774
- if (!instancePath) return { error: "Instance path is required" };
775
-
776
- const maxDepth = (requestData.maxDepth as number) ?? 10;
777
- const classFilter = requestData.classFilter as string | undefined;
778
-
779
- const instance = getInstanceByPath(instancePath);
780
- if (!instance) return { error: `Instance not found: ${instancePath}` };
781
-
782
- const descendants: { name: string; className: string; path: string; depth: number }[] = [];
783
-
784
- function collect(inst: Instance, depth: number) {
785
- if (depth > maxDepth) return;
786
- for (const child of inst.GetChildren()) {
787
- if (classFilter && !child.IsA(classFilter as keyof Instances)) continue;
788
- descendants.push({
789
- name: child.Name,
790
- className: child.ClassName,
791
- path: getInstancePath(child),
792
- depth,
793
- });
794
- collect(child, depth + 1);
795
- }
796
- }
797
-
798
- collect(instance, 1);
799
-
800
- return { instancePath, descendants, count: descendants.size(), maxDepth };
801
- }
802
-
803
- function compareInstances(requestData: Record<string, unknown>) {
804
- const instancePathA = requestData.instancePathA as string;
805
- const instancePathB = requestData.instancePathB as string;
806
-
807
- if (!instancePathA || !instancePathB) {
808
- return { error: "Both instancePathA and instancePathB are required" };
809
- }
810
-
811
- const instA = getInstanceByPath(instancePathA);
812
- if (!instA) return { error: `Instance not found: ${instancePathA}` };
813
-
814
- const instB = getInstanceByPath(instancePathB);
815
- if (!instB) return { error: `Instance not found: ${instancePathB}` };
816
-
817
- const commonProps = [
818
- "Name", "ClassName",
819
- "Size", "Position", "Rotation", "CFrame", "Anchored", "CanCollide",
820
- "Transparency", "BrickColor", "Material", "Color", "Text", "TextColor3",
821
- "BackgroundColor3", "Image", "ImageColor3", "Visible", "Active", "ZIndex",
822
- "BorderSizePixel", "BackgroundTransparency", "ImageTransparency",
823
- "TextTransparency", "Value", "Enabled", "Brightness", "Range", "Shadows",
824
- ];
825
-
826
- const matching: Record<string, string> = {};
827
- const differing: Record<string, { a: string; b: string }> = {};
828
- const onlyA: string[] = [];
829
- const onlyB: string[] = [];
830
-
831
- for (const prop of commonProps) {
832
- const [okA, valA] = pcall(() => tostring((instA as unknown as Record<string, unknown>)[prop]));
833
- const [okB, valB] = pcall(() => tostring((instB as unknown as Record<string, unknown>)[prop]));
834
-
835
- if (okA && okB) {
836
- if (valA === valB) {
837
- matching[prop] = valA as string;
838
- } else {
839
- differing[prop] = { a: valA as string, b: valB as string };
840
- }
841
- } else if (okA) {
842
- onlyA.push(prop);
843
- } else if (okB) {
844
- onlyB.push(prop);
845
- }
846
- }
847
-
848
- return {
849
- instancePathA,
850
- instancePathB,
851
- classNameA: instA.ClassName,
852
- classNameB: instB.ClassName,
853
- matching,
854
- differing,
855
- onlyA,
856
- onlyB,
857
- };
858
- }
859
-
860
- export = {
861
- getFileTree,
862
- searchFiles,
863
- getPlaceInfo,
864
- getServices,
865
- searchObjects,
866
- getInstanceProperties,
867
- getInstanceChildren,
868
- searchByProperty,
869
- getClassInfo,
870
- getProjectStructure,
871
- grepScripts,
872
- getDescendants,
873
- compareInstances,
874
- };