@chrrxs/robloxstudio-mcp 3.0.0 → 3.0.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/dist/index.js +9401 -9047
- package/package.json +2 -2
- package/studio-plugin/MCPPlugin.rbxmx +400 -97
- package/studio-plugin/.Carbon.rbxm.lock +0 -0
- package/studio-plugin/Carbon.rbxm +0 -0
- package/studio-plugin/INSTALLATION.md +0 -170
- package/studio-plugin/MCPInspectorPlugin.rbxmx +0 -169759
- package/studio-plugin/default.project.json +0 -19
- package/studio-plugin/dev.project.json +0 -23
- package/studio-plugin/include/LibMP.lua +0 -156378
- package/studio-plugin/inspector-icon.png +0 -0
- package/studio-plugin/package-lock.json +0 -706
- package/studio-plugin/package.json +0 -19
- package/studio-plugin/plugin.json +0 -10
- package/studio-plugin/src/modules/AssetSanitizationPolicy.ts +0 -127
- package/studio-plugin/src/modules/ClientBroker.ts +0 -450
- package/studio-plugin/src/modules/Communication.ts +0 -601
- package/studio-plugin/src/modules/EvalBridges.ts +0 -255
- package/studio-plugin/src/modules/HttpDiagnostics.ts +0 -50
- package/studio-plugin/src/modules/LuauExec.ts +0 -403
- package/studio-plugin/src/modules/Recording.ts +0 -28
- package/studio-plugin/src/modules/RenderMonitor.ts +0 -60
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +0 -210
- package/studio-plugin/src/modules/ServerUrlSettings.ts +0 -117
- package/studio-plugin/src/modules/State.ts +0 -39
- package/studio-plugin/src/modules/StopPlayMonitor.ts +0 -267
- package/studio-plugin/src/modules/UI.ts +0 -597
- package/studio-plugin/src/modules/Utils.ts +0 -527
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +0 -391
- package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +0 -460
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +0 -170
- package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +0 -149
- package/studio-plugin/src/modules/handlers/GenerateModelHandlers.ts +0 -168
- package/studio-plugin/src/modules/handlers/InputHandlers.ts +0 -163
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +0 -14
- package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +0 -44
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +0 -96
- package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +0 -1263
- package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +0 -62
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -716
- package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +0 -216
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +0 -531
- package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +0 -386
- package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +0 -172
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +0 -350
- package/studio-plugin/src/server/index.server.ts +0 -135
- package/studio-plugin/src/types/index.d.ts +0 -57
- package/studio-plugin/tsconfig.json +0 -20
|
@@ -1,716 +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 searchObjects(requestData: Record<string, unknown>) {
|
|
127
|
-
const query = requestData.query as string;
|
|
128
|
-
const searchType = (requestData.searchType as string) ?? "name";
|
|
129
|
-
const propertyName = requestData.propertyName as string | undefined;
|
|
130
|
-
|
|
131
|
-
if (!query) return { error: "Query is required" };
|
|
132
|
-
|
|
133
|
-
const results: { name: string; className: string; path: string }[] = [];
|
|
134
|
-
|
|
135
|
-
function searchRecursive(instance: Instance) {
|
|
136
|
-
let match = false;
|
|
137
|
-
|
|
138
|
-
if (searchType === "name") {
|
|
139
|
-
match = instance.Name.lower().find(query.lower())[0] !== undefined;
|
|
140
|
-
} else if (searchType === "class") {
|
|
141
|
-
match = instance.ClassName.lower().find(query.lower())[0] !== undefined;
|
|
142
|
-
} else if (searchType === "property" && propertyName) {
|
|
143
|
-
const [success, value] = pcall(() => tostring((instance as unknown as Record<string, unknown>)[propertyName]));
|
|
144
|
-
if (success) {
|
|
145
|
-
match = (value as string).lower().find(query.lower())[0] !== undefined;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (match) {
|
|
150
|
-
results.push({
|
|
151
|
-
name: instance.Name,
|
|
152
|
-
className: instance.ClassName,
|
|
153
|
-
path: getInstancePath(instance),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
for (const child of instance.GetChildren()) {
|
|
158
|
-
searchRecursive(child);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
searchRecursive(game);
|
|
163
|
-
|
|
164
|
-
return { results, query, searchType, count: results.size() };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function getInstanceProperties(requestData: Record<string, unknown>) {
|
|
168
|
-
const instancePath = requestData.instancePath as string;
|
|
169
|
-
const excludeSource = (requestData.excludeSource as boolean) ?? false;
|
|
170
|
-
if (!instancePath) return { error: "Instance path is required" };
|
|
171
|
-
|
|
172
|
-
const instance = getInstanceByPath(instancePath);
|
|
173
|
-
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
174
|
-
|
|
175
|
-
const properties: Record<string, unknown> = {};
|
|
176
|
-
const [success, result] = pcall(() => {
|
|
177
|
-
const basicProps = ["Name", "ClassName", "Parent"];
|
|
178
|
-
for (const prop of basicProps) {
|
|
179
|
-
const [propSuccess, propValue] = pcall(() => {
|
|
180
|
-
const val = (instance as unknown as Record<string, unknown>)[prop];
|
|
181
|
-
if (prop === "Parent" && val) return getInstancePath(val as Instance);
|
|
182
|
-
if (val === undefined) return "nil";
|
|
183
|
-
return tostring(val);
|
|
184
|
-
});
|
|
185
|
-
if (propSuccess) properties[prop] = propValue;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const commonProps = [
|
|
189
|
-
"Size", "Position", "Rotation", "CFrame", "Anchored", "CanCollide",
|
|
190
|
-
"Transparency", "BrickColor", "Material", "Color", "Text", "TextColor3",
|
|
191
|
-
"BackgroundColor3", "Image", "ImageColor3", "Visible", "Active", "ZIndex",
|
|
192
|
-
"BorderSizePixel", "BackgroundTransparency", "ImageTransparency",
|
|
193
|
-
"TextTransparency", "Value", "Enabled", "Brightness", "Range", "Shadows",
|
|
194
|
-
"Face", "SurfaceType",
|
|
195
|
-
];
|
|
196
|
-
|
|
197
|
-
for (const prop of commonProps) {
|
|
198
|
-
const [propSuccess, propValue] = pcall(() => {
|
|
199
|
-
const val = (instance as unknown as Record<string, unknown>)[prop];
|
|
200
|
-
if (typeOf(val) === "UDim2") {
|
|
201
|
-
const udim = val as UDim2;
|
|
202
|
-
return {
|
|
203
|
-
X: { Scale: udim.X.Scale, Offset: udim.X.Offset },
|
|
204
|
-
Y: { Scale: udim.Y.Scale, Offset: udim.Y.Offset },
|
|
205
|
-
_type: "UDim2",
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
return tostring(val);
|
|
209
|
-
});
|
|
210
|
-
if (propSuccess) properties[prop] = propValue;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (instance.IsA("LuaSourceContainer")) {
|
|
214
|
-
if (!excludeSource) {
|
|
215
|
-
properties.Source = readScriptSource(instance);
|
|
216
|
-
} else {
|
|
217
|
-
const src = readScriptSource(instance);
|
|
218
|
-
properties.SourceLength = src.size();
|
|
219
|
-
properties.LineCount = Utils.splitLines(src)[0].size();
|
|
220
|
-
}
|
|
221
|
-
if (instance.IsA("BaseScript")) {
|
|
222
|
-
properties.Enabled = tostring(instance.Enabled);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (instance.IsA("Part")) {
|
|
227
|
-
properties.Shape = tostring(instance.Shape);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (instance.IsA("BasePart")) {
|
|
231
|
-
properties.TopSurface = tostring(instance.TopSurface);
|
|
232
|
-
properties.BottomSurface = tostring(instance.BottomSurface);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (instance.IsA("MeshPart")) {
|
|
236
|
-
properties.MeshId = tostring(instance.MeshId);
|
|
237
|
-
properties.TextureID = tostring(instance.TextureID);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (instance.IsA("SpecialMesh")) {
|
|
241
|
-
properties.MeshId = tostring(instance.MeshId);
|
|
242
|
-
properties.TextureId = tostring(instance.TextureId);
|
|
243
|
-
properties.MeshType = tostring(instance.MeshType);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (instance.IsA("Sound")) {
|
|
247
|
-
properties.SoundId = tostring(instance.SoundId);
|
|
248
|
-
properties.TimeLength = tostring(instance.TimeLength);
|
|
249
|
-
properties.IsPlaying = tostring(instance.IsPlaying);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (instance.IsA("Animation")) {
|
|
253
|
-
properties.AnimationId = tostring(instance.AnimationId);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (instance.IsA("Decal") || instance.IsA("Texture")) {
|
|
257
|
-
properties.Texture = tostring((instance as Decal | Texture).Texture);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (instance.IsA("Shirt")) {
|
|
261
|
-
properties.ShirtTemplate = tostring(instance.ShirtTemplate);
|
|
262
|
-
} else if (instance.IsA("Pants")) {
|
|
263
|
-
properties.PantsTemplate = tostring(instance.PantsTemplate);
|
|
264
|
-
} else if (instance.IsA("ShirtGraphic")) {
|
|
265
|
-
properties.Graphic = tostring(instance.Graphic);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
properties.ChildCount = tostring(instance.GetChildren().size());
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
if (success) {
|
|
272
|
-
return { instancePath, className: instance.ClassName, properties };
|
|
273
|
-
} else {
|
|
274
|
-
return { error: `Failed to get properties: ${result}` };
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function searchByProperty(requestData: Record<string, unknown>) {
|
|
279
|
-
const propertyName = requestData.propertyName as string;
|
|
280
|
-
const propertyValue = requestData.propertyValue as string;
|
|
281
|
-
|
|
282
|
-
if (!propertyName || !propertyValue) {
|
|
283
|
-
return { error: "Property name and value are required" };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const results: { name: string; className: string; path: string; propertyValue: string }[] = [];
|
|
287
|
-
|
|
288
|
-
function searchRecursive(instance: Instance) {
|
|
289
|
-
const [success, value] = pcall(() => tostring((instance as unknown as Record<string, unknown>)[propertyName]));
|
|
290
|
-
if (success && (value as string).lower().find(propertyValue.lower())[0] !== undefined) {
|
|
291
|
-
results.push({
|
|
292
|
-
name: instance.Name,
|
|
293
|
-
className: instance.ClassName,
|
|
294
|
-
path: getInstancePath(instance),
|
|
295
|
-
propertyValue: value as string,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
for (const child of instance.GetChildren()) {
|
|
299
|
-
searchRecursive(child);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
searchRecursive(game);
|
|
304
|
-
return { propertyName, propertyValue, results, count: results.size() };
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function getClassInfo(requestData: Record<string, unknown>) {
|
|
308
|
-
const className = requestData.className as string;
|
|
309
|
-
if (!className) return { error: "Class name is required" };
|
|
310
|
-
|
|
311
|
-
let [success, tempInstance] = pcall(() => new Instance(className as keyof CreatableInstances));
|
|
312
|
-
let isService = false;
|
|
313
|
-
|
|
314
|
-
if (!success) {
|
|
315
|
-
const [serviceSuccess, serviceInstance] = pcall(() =>
|
|
316
|
-
game.GetService(className as keyof Services),
|
|
317
|
-
);
|
|
318
|
-
if (serviceSuccess && serviceInstance) {
|
|
319
|
-
success = true;
|
|
320
|
-
tempInstance = serviceInstance as unknown as Instance;
|
|
321
|
-
isService = true;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (!success) return { error: `Invalid class name: ${className}` };
|
|
326
|
-
|
|
327
|
-
const classInfo: {
|
|
328
|
-
className: string;
|
|
329
|
-
isService: boolean;
|
|
330
|
-
properties: string[];
|
|
331
|
-
methods: string[];
|
|
332
|
-
events: string[];
|
|
333
|
-
} = { className, isService, properties: [], methods: [], events: [] };
|
|
334
|
-
|
|
335
|
-
const commonProps = [
|
|
336
|
-
"Name", "ClassName", "Parent", "Size", "Position", "Rotation", "CFrame",
|
|
337
|
-
"Anchored", "CanCollide", "Transparency", "BrickColor", "Material", "Color",
|
|
338
|
-
"Text", "TextColor3", "BackgroundColor3", "Image", "ImageColor3", "Visible",
|
|
339
|
-
"Active", "ZIndex", "BorderSizePixel", "BackgroundTransparency",
|
|
340
|
-
"ImageTransparency", "TextTransparency", "Value", "Enabled", "Brightness",
|
|
341
|
-
"Range", "Shadows",
|
|
342
|
-
];
|
|
343
|
-
|
|
344
|
-
for (const prop of commonProps) {
|
|
345
|
-
const [propSuccess] = pcall(() => (tempInstance as unknown as Record<string, unknown>)[prop]);
|
|
346
|
-
if (propSuccess) classInfo.properties.push(prop);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const commonMethods = [
|
|
350
|
-
"Destroy", "Clone", "FindFirstChild", "FindFirstChildOfClass",
|
|
351
|
-
"GetChildren", "IsA", "IsAncestorOf", "IsDescendantOf", "WaitForChild",
|
|
352
|
-
];
|
|
353
|
-
|
|
354
|
-
for (const method of commonMethods) {
|
|
355
|
-
const [methodSuccess] = pcall(() => (tempInstance as unknown as Record<string, unknown>)[method]);
|
|
356
|
-
if (methodSuccess) classInfo.methods.push(method);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (!isService) {
|
|
360
|
-
(tempInstance as Instance).Destroy();
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return classInfo;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function getProjectStructure(requestData: Record<string, unknown>) {
|
|
367
|
-
const startPath = (requestData.path as string) ?? "";
|
|
368
|
-
const maxDepth = (requestData.maxDepth as number) ?? 3;
|
|
369
|
-
const showScriptsOnly = (requestData.scriptsOnly as boolean) ?? false;
|
|
370
|
-
|
|
371
|
-
if (startPath === "" || startPath === "game") {
|
|
372
|
-
const services: Record<string, unknown>[] = [];
|
|
373
|
-
const mainServices = [
|
|
374
|
-
"Workspace", "ServerScriptService", "ServerStorage", "ReplicatedStorage",
|
|
375
|
-
"StarterGui", "StarterPack", "StarterPlayer", "Players",
|
|
376
|
-
];
|
|
377
|
-
|
|
378
|
-
for (const serviceName of mainServices) {
|
|
379
|
-
const [svcOk, service] = pcall(() => game.GetService(serviceName as keyof Services));
|
|
380
|
-
if (svcOk && service) {
|
|
381
|
-
services.push({
|
|
382
|
-
name: service.Name,
|
|
383
|
-
className: service.ClassName,
|
|
384
|
-
path: getInstancePath(service as Instance),
|
|
385
|
-
childCount: (service as Instance).GetChildren().size(),
|
|
386
|
-
hasChildren: (service as Instance).GetChildren().size() > 0,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return {
|
|
392
|
-
type: "service_overview",
|
|
393
|
-
services,
|
|
394
|
-
timestamp: tick(),
|
|
395
|
-
note: "Use path parameter to explore specific locations (e.g., 'game.ServerScriptService')",
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const startInstance = getInstanceByPath(startPath);
|
|
400
|
-
if (!startInstance) return { error: `Path not found: ${startPath}` };
|
|
401
|
-
|
|
402
|
-
function getStructure(instance: Instance, depth: number): Record<string, unknown> {
|
|
403
|
-
if (depth > maxDepth) {
|
|
404
|
-
return {
|
|
405
|
-
name: instance.Name,
|
|
406
|
-
className: instance.ClassName,
|
|
407
|
-
path: getInstancePath(instance),
|
|
408
|
-
childCount: instance.GetChildren().size(),
|
|
409
|
-
hasMore: true,
|
|
410
|
-
note: "Max depth reached - use this path to explore further",
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const node: Record<string, unknown> = {
|
|
415
|
-
name: instance.Name,
|
|
416
|
-
className: instance.ClassName,
|
|
417
|
-
path: getInstancePath(instance),
|
|
418
|
-
children: [] as Record<string, unknown>[],
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
if (instance.IsA("LuaSourceContainer")) {
|
|
422
|
-
node.hasSource = true;
|
|
423
|
-
node.scriptType = instance.ClassName;
|
|
424
|
-
if (instance.IsA("BaseScript")) {
|
|
425
|
-
node.enabled = instance.Enabled;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (instance.IsA("GuiObject")) {
|
|
430
|
-
node.visible = instance.Visible;
|
|
431
|
-
if (instance.IsA("Frame") || instance.IsA("ScreenGui")) {
|
|
432
|
-
node.guiType = "container";
|
|
433
|
-
} else if (instance.IsA("TextLabel") || instance.IsA("TextButton")) {
|
|
434
|
-
node.guiType = "text";
|
|
435
|
-
const textInst = instance as TextLabel | TextButton;
|
|
436
|
-
if (textInst.Text !== "") node.text = textInst.Text;
|
|
437
|
-
} else if (instance.IsA("ImageLabel") || instance.IsA("ImageButton")) {
|
|
438
|
-
node.guiType = "image";
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
let children = instance.GetChildren();
|
|
443
|
-
if (showScriptsOnly) {
|
|
444
|
-
children = children.filter(
|
|
445
|
-
(child) => child.IsA("BaseScript") || child.IsA("Folder") || child.IsA("ModuleScript"),
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const nodeChildren = node.children as Record<string, unknown>[];
|
|
450
|
-
const childCount = children.size();
|
|
451
|
-
if (childCount > 20 && depth < maxDepth) {
|
|
452
|
-
const classGroups = new Map<string, Instance[]>();
|
|
453
|
-
for (const child of children) {
|
|
454
|
-
const cn = child.ClassName;
|
|
455
|
-
if (!classGroups.has(cn)) classGroups.set(cn, []);
|
|
456
|
-
classGroups.get(cn)!.push(child);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const childSummary: Record<string, unknown>[] = [];
|
|
460
|
-
classGroups.forEach((classChildren, cn) => {
|
|
461
|
-
childSummary.push({
|
|
462
|
-
className: cn,
|
|
463
|
-
count: classChildren.size(),
|
|
464
|
-
examples: [classChildren[0]?.Name, classChildren[1]?.Name],
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
node.childSummary = childSummary;
|
|
468
|
-
|
|
469
|
-
classGroups.forEach((classChildren, cn) => {
|
|
470
|
-
const limit = math.min(3, classChildren.size());
|
|
471
|
-
for (let i = 0; i < limit; i++) {
|
|
472
|
-
nodeChildren.push(getStructure(classChildren[i], depth + 1));
|
|
473
|
-
}
|
|
474
|
-
if (classChildren.size() > 3) {
|
|
475
|
-
nodeChildren.push({
|
|
476
|
-
name: `... ${classChildren.size() - 3} more ${cn} objects`,
|
|
477
|
-
className: "MoreIndicator",
|
|
478
|
-
path: `${getInstancePath(instance)} [${cn} children]`,
|
|
479
|
-
note: "Use specific path to explore these objects",
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
} else {
|
|
484
|
-
for (const child of children) {
|
|
485
|
-
nodeChildren.push(getStructure(child, depth + 1));
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return node;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const result = getStructure(startInstance, 0);
|
|
493
|
-
result.requestedPath = startPath;
|
|
494
|
-
result.maxDepth = maxDepth;
|
|
495
|
-
result.scriptsOnly = showScriptsOnly;
|
|
496
|
-
result.timestamp = tick();
|
|
497
|
-
|
|
498
|
-
return result;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Split a Lua pattern on TOP-LEVEL "|" into alternatives. Lua patterns have no
|
|
502
|
-
// alternation operator, so "foo|bar" would otherwise be matched as the literal
|
|
503
|
-
// text "foo|bar" and silently never hit. "%|" stays a literal pipe, and "%bxy"
|
|
504
|
-
// keeps both balanced-match delimiter characters.
|
|
505
|
-
function splitLuaAlternation(pattern: string): string[] {
|
|
506
|
-
const parts: string[] = [];
|
|
507
|
-
let current = "";
|
|
508
|
-
let i = 1;
|
|
509
|
-
const n = pattern.size();
|
|
510
|
-
let inCharClass = false;
|
|
511
|
-
while (i <= n) {
|
|
512
|
-
const c = string.sub(pattern, i, i);
|
|
513
|
-
if (c === "%") {
|
|
514
|
-
if (string.sub(pattern, i + 1, i + 1) === "b") {
|
|
515
|
-
current += string.sub(pattern, i, math.min(i + 3, n));
|
|
516
|
-
i += 4;
|
|
517
|
-
continue;
|
|
518
|
-
}
|
|
519
|
-
// Preserve an escape pair (e.g. %|, %., %d) intact.
|
|
520
|
-
current += string.sub(pattern, i, i + 1);
|
|
521
|
-
i += 2;
|
|
522
|
-
} else if (c === "[") {
|
|
523
|
-
inCharClass = true;
|
|
524
|
-
current += c;
|
|
525
|
-
i += 1;
|
|
526
|
-
} else if (c === "]") {
|
|
527
|
-
inCharClass = false;
|
|
528
|
-
current += c;
|
|
529
|
-
i += 1;
|
|
530
|
-
} else if (c === "|" && !inCharClass) {
|
|
531
|
-
parts.push(current);
|
|
532
|
-
current = "";
|
|
533
|
-
i += 1;
|
|
534
|
-
} else {
|
|
535
|
-
current += c;
|
|
536
|
-
i += 1;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
parts.push(current);
|
|
540
|
-
return parts;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Return the earliest match across alternatives (mirrors regex alternation).
|
|
544
|
-
function findFirstPattern(line: string, alternatives: string[]): [number | undefined, number | undefined] {
|
|
545
|
-
let bestStart: number | undefined;
|
|
546
|
-
let bestEnd: number | undefined;
|
|
547
|
-
for (const alt of alternatives) {
|
|
548
|
-
if (alt === "") continue;
|
|
549
|
-
const [s, e] = string.find(line, alt);
|
|
550
|
-
if (s !== undefined && (bestStart === undefined || s < bestStart)) {
|
|
551
|
-
bestStart = s;
|
|
552
|
-
bestEnd = e as number;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
return [bestStart, bestEnd];
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function grepScripts(requestData: Record<string, unknown>) {
|
|
559
|
-
const pattern = requestData.pattern as string;
|
|
560
|
-
if (!pattern) return { error: "pattern is required" };
|
|
561
|
-
|
|
562
|
-
const usePattern = (requestData.usePattern as boolean) ?? false;
|
|
563
|
-
if (usePattern && requestData.caseSensitive === false) {
|
|
564
|
-
return {
|
|
565
|
-
error: "Case-insensitive Lua pattern search is not supported. Omit caseSensitive or pass caseSensitive: true with usePattern: true, or use literal search.",
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const caseSensitive = usePattern ? true : ((requestData.caseSensitive as boolean) ?? false);
|
|
570
|
-
const contextLines = (requestData.contextLines as number) ?? 0;
|
|
571
|
-
const maxResults = (requestData.maxResults as number) ?? 100;
|
|
572
|
-
const maxResultsPerScript = (requestData.maxResultsPerScript as number) ?? 0;
|
|
573
|
-
const filesOnly = (requestData.filesOnly as boolean) ?? false;
|
|
574
|
-
const searchPath = (requestData.path as string) ?? "";
|
|
575
|
-
const classFilter = requestData.classFilter as string | undefined;
|
|
576
|
-
|
|
577
|
-
const startInstance = searchPath !== "" ? getInstanceByPath(searchPath) : game;
|
|
578
|
-
if (!startInstance) return { error: `Path not found: ${searchPath}` };
|
|
579
|
-
|
|
580
|
-
// Prepare pattern for matching
|
|
581
|
-
const searchPattern = caseSensitive ? pattern : pattern.lower();
|
|
582
|
-
// Pre-split top-level "|" alternation once (pattern mode only).
|
|
583
|
-
const patternAlternatives = usePattern ? splitLuaAlternation(searchPattern) : undefined;
|
|
584
|
-
|
|
585
|
-
interface LineMatch {
|
|
586
|
-
line: number;
|
|
587
|
-
column: number;
|
|
588
|
-
text: string;
|
|
589
|
-
before: string[];
|
|
590
|
-
after: string[];
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
interface ScriptResult {
|
|
594
|
-
instancePath: string;
|
|
595
|
-
name: string;
|
|
596
|
-
className: string;
|
|
597
|
-
enabled?: boolean;
|
|
598
|
-
matches: LineMatch[];
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const results: ScriptResult[] = [];
|
|
602
|
-
let totalMatches = 0;
|
|
603
|
-
let scriptsSearched = 0;
|
|
604
|
-
let hitLimit = false;
|
|
605
|
-
|
|
606
|
-
function searchInstance(instance: Instance) {
|
|
607
|
-
if (hitLimit) return;
|
|
608
|
-
|
|
609
|
-
if (instance.IsA("LuaSourceContainer")) {
|
|
610
|
-
// Apply class filter
|
|
611
|
-
if (classFilter) {
|
|
612
|
-
if (!instance.ClassName.lower().find(classFilter.lower())[0]) return;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
scriptsSearched++;
|
|
616
|
-
const source = readScriptSource(instance);
|
|
617
|
-
const [lines] = Utils.splitLines(source);
|
|
618
|
-
const scriptMatches: LineMatch[] = [];
|
|
619
|
-
let scriptMatchCount = 0;
|
|
620
|
-
|
|
621
|
-
for (let i = 0; i < lines.size(); i++) {
|
|
622
|
-
if (hitLimit) break;
|
|
623
|
-
if (maxResultsPerScript > 0 && scriptMatchCount >= maxResultsPerScript) break;
|
|
624
|
-
|
|
625
|
-
const line = lines[i];
|
|
626
|
-
const searchLine = caseSensitive ? line : line.lower();
|
|
627
|
-
|
|
628
|
-
let matchStart: number | undefined;
|
|
629
|
-
let matchEnd: number | undefined;
|
|
630
|
-
|
|
631
|
-
if (usePattern) {
|
|
632
|
-
[matchStart, matchEnd] = findFirstPattern(searchLine, patternAlternatives!);
|
|
633
|
-
} else {
|
|
634
|
-
[matchStart, matchEnd] = string.find(searchLine, searchPattern, 1, true);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
if (matchStart !== undefined) {
|
|
638
|
-
scriptMatchCount++;
|
|
639
|
-
totalMatches++;
|
|
640
|
-
|
|
641
|
-
if (totalMatches > maxResults) {
|
|
642
|
-
hitLimit = true;
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
if (!filesOnly) {
|
|
647
|
-
// Gather context lines
|
|
648
|
-
const before: string[] = [];
|
|
649
|
-
const after: string[] = [];
|
|
650
|
-
|
|
651
|
-
if (contextLines > 0) {
|
|
652
|
-
const beforeStart = math.max(0, i - contextLines);
|
|
653
|
-
for (let j = beforeStart; j < i; j++) {
|
|
654
|
-
before.push(lines[j]);
|
|
655
|
-
}
|
|
656
|
-
const afterEnd = math.min(lines.size() - 1, i + contextLines);
|
|
657
|
-
for (let j = i + 1; j <= afterEnd; j++) {
|
|
658
|
-
after.push(lines[j]);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
scriptMatches.push({
|
|
663
|
-
line: i + 1, // 1-indexed
|
|
664
|
-
column: matchStart,
|
|
665
|
-
text: line,
|
|
666
|
-
before,
|
|
667
|
-
after,
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
if (scriptMatchCount > 0) {
|
|
674
|
-
const scriptResult: ScriptResult = {
|
|
675
|
-
instancePath: getInstancePath(instance),
|
|
676
|
-
name: instance.Name,
|
|
677
|
-
className: instance.ClassName,
|
|
678
|
-
matches: scriptMatches,
|
|
679
|
-
};
|
|
680
|
-
if (instance.IsA("BaseScript")) {
|
|
681
|
-
scriptResult.enabled = instance.Enabled;
|
|
682
|
-
}
|
|
683
|
-
results.push(scriptResult);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
for (const child of instance.GetChildren()) {
|
|
688
|
-
if (hitLimit) return;
|
|
689
|
-
searchInstance(child);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
searchInstance(startInstance);
|
|
694
|
-
|
|
695
|
-
return {
|
|
696
|
-
results,
|
|
697
|
-
pattern,
|
|
698
|
-
totalMatches: hitLimit ? `>${maxResults}` : totalMatches,
|
|
699
|
-
scriptsSearched,
|
|
700
|
-
scriptsMatched: results.size(),
|
|
701
|
-
truncated: hitLimit,
|
|
702
|
-
options: { caseSensitive, contextLines, usePattern, filesOnly, maxResults, maxResultsPerScript },
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
export = {
|
|
707
|
-
getFileTree,
|
|
708
|
-
searchFiles,
|
|
709
|
-
getPlaceInfo,
|
|
710
|
-
searchObjects,
|
|
711
|
-
getInstanceProperties,
|
|
712
|
-
searchByProperty,
|
|
713
|
-
getClassInfo,
|
|
714
|
-
getProjectStructure,
|
|
715
|
-
grepScripts,
|
|
716
|
-
};
|