@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,531 +0,0 @@
|
|
|
1
|
-
import Utils from "../Utils";
|
|
2
|
-
import Recording from "../Recording";
|
|
3
|
-
|
|
4
|
-
const { getInstancePath, getInstanceByPath, readScriptSource, applyScriptSource, splitLines, joinLines } = Utils;
|
|
5
|
-
const { beginRecording, finishRecording } = Recording;
|
|
6
|
-
|
|
7
|
-
const SOURCE_TRUNCATE_CHAR_BUDGET = 25000;
|
|
8
|
-
const SOURCE_TRUNCATE_LINE_BUDGET = 400;
|
|
9
|
-
const SOURCE_TRUNCATE_TO_LINES = 300;
|
|
10
|
-
|
|
11
|
-
function normalizeEscapes(s: string): string {
|
|
12
|
-
let result = s;
|
|
13
|
-
result = result.gsub("\\\\", "\x01")[0];
|
|
14
|
-
result = result.gsub("\\n", "\n")[0];
|
|
15
|
-
result = result.gsub("\\t", "\t")[0];
|
|
16
|
-
result = result.gsub("\\r", "\r")[0];
|
|
17
|
-
result = result.gsub('\\"', '"')[0];
|
|
18
|
-
result = result.gsub("\x01", "\\")[0];
|
|
19
|
-
return result;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getTopServiceName(instance: Instance): string {
|
|
23
|
-
let topServiceInst: Instance = instance;
|
|
24
|
-
while (topServiceInst.Parent && topServiceInst.Parent !== game) {
|
|
25
|
-
topServiceInst = topServiceInst.Parent;
|
|
26
|
-
}
|
|
27
|
-
return topServiceInst.Name;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function sliceLines(lines: string[], startLine: number, endLine: number): string[] {
|
|
31
|
-
const selectedLines: string[] = [];
|
|
32
|
-
for (let i = startLine; i <= endLine; i++) {
|
|
33
|
-
selectedLines.push(lines[i - 1] ?? "");
|
|
34
|
-
}
|
|
35
|
-
return selectedLines;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function numberLines(lines: string[], lineOffset: number): string {
|
|
39
|
-
const numberedLines: string[] = [];
|
|
40
|
-
for (let i = 0; i < lines.size(); i++) {
|
|
41
|
-
numberedLines.push(`${i + lineOffset}: ${lines[i]}`);
|
|
42
|
-
}
|
|
43
|
-
return numberedLines.join("\n");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getScriptSource(requestData: Record<string, unknown>) {
|
|
47
|
-
const instancePath = requestData.instancePath as string;
|
|
48
|
-
const startLine = requestData.startLine as number | undefined;
|
|
49
|
-
const endLine = requestData.endLine as number | undefined;
|
|
50
|
-
|
|
51
|
-
if (!instancePath) return { error: "Instance path is required" };
|
|
52
|
-
|
|
53
|
-
const instance = getInstanceByPath(instancePath);
|
|
54
|
-
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
55
|
-
if (!instance.IsA("LuaSourceContainer")) {
|
|
56
|
-
return { error: `Instance is not a script-like object: ${instance.ClassName}` };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const [success, result] = pcall(() => {
|
|
60
|
-
const fullSource = readScriptSource(instance);
|
|
61
|
-
const [lines, hasTrailingNewline] = splitLines(fullSource);
|
|
62
|
-
const totalLineCount = lines.size();
|
|
63
|
-
const explicitRange = startLine !== undefined || endLine !== undefined;
|
|
64
|
-
const shouldTruncate = !explicitRange &&
|
|
65
|
-
(fullSource.size() > SOURCE_TRUNCATE_CHAR_BUDGET || totalLineCount > SOURCE_TRUNCATE_LINE_BUDGET);
|
|
66
|
-
const returnedStartLine = explicitRange ? math.max(1, startLine ?? 1) : 1;
|
|
67
|
-
const returnedEndLine = shouldTruncate
|
|
68
|
-
? math.min(SOURCE_TRUNCATE_TO_LINES, totalLineCount)
|
|
69
|
-
: explicitRange ? math.min(totalLineCount, endLine ?? totalLineCount) : totalLineCount;
|
|
70
|
-
const selectedLines = (explicitRange || shouldTruncate)
|
|
71
|
-
? sliceLines(lines, returnedStartLine, returnedEndLine)
|
|
72
|
-
: lines;
|
|
73
|
-
const sourceToReturn = explicitRange
|
|
74
|
-
? joinLines(selectedLines, hasTrailingNewline && returnedEndLine === totalLineCount)
|
|
75
|
-
: shouldTruncate ? selectedLines.join("\n") : fullSource;
|
|
76
|
-
|
|
77
|
-
const resp: Record<string, unknown> = {
|
|
78
|
-
instancePath,
|
|
79
|
-
className: instance.ClassName,
|
|
80
|
-
name: instance.Name,
|
|
81
|
-
source: sourceToReturn,
|
|
82
|
-
numberedSource: numberLines(selectedLines, returnedStartLine),
|
|
83
|
-
sourceLength: fullSource.size(),
|
|
84
|
-
lineCount: totalLineCount,
|
|
85
|
-
startLine: returnedStartLine,
|
|
86
|
-
endLine: returnedEndLine,
|
|
87
|
-
isPartial: explicitRange,
|
|
88
|
-
truncated: shouldTruncate,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
if (shouldTruncate) {
|
|
92
|
-
resp.note = `Script truncated to first ${returnedEndLine} of ${totalLineCount} lines (${fullSource.size()} chars). Use line_range to read specific sections.`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (instance.IsA("BaseScript")) {
|
|
96
|
-
resp.enabled = instance.Enabled;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
resp.topService = getTopServiceName(instance);
|
|
100
|
-
|
|
101
|
-
return resp;
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (success) {
|
|
105
|
-
return result;
|
|
106
|
-
} else {
|
|
107
|
-
return { error: `Failed to get script source: ${result}` };
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function setScriptSource(requestData: Record<string, unknown>) {
|
|
112
|
-
const instancePath = requestData.instancePath as string;
|
|
113
|
-
const newSource = requestData.source as string;
|
|
114
|
-
|
|
115
|
-
if (!instancePath || !newSource) return { error: "Instance path and source are required" };
|
|
116
|
-
|
|
117
|
-
const instance = getInstanceByPath(instancePath);
|
|
118
|
-
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
119
|
-
if (!instance.IsA("LuaSourceContainer")) {
|
|
120
|
-
return { error: `Instance is not a script-like object: ${instance.ClassName}` };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const sourceToSet = normalizeEscapes(newSource);
|
|
124
|
-
const recordingId = beginRecording(`Set script source: ${instance.Name}`);
|
|
125
|
-
|
|
126
|
-
const [readSuccess, readResult] = pcall(() => readScriptSource(instance).size());
|
|
127
|
-
if (!readSuccess) {
|
|
128
|
-
finishRecording(recordingId, false);
|
|
129
|
-
return { error: `Failed to read script source before updating: ${readResult}` };
|
|
130
|
-
}
|
|
131
|
-
const oldSourceLength = readResult as number;
|
|
132
|
-
const applyResult = applyScriptSource(instance, sourceToSet);
|
|
133
|
-
|
|
134
|
-
if (applyResult.success) {
|
|
135
|
-
finishRecording(recordingId, true);
|
|
136
|
-
return {
|
|
137
|
-
success: true, instancePath,
|
|
138
|
-
oldSourceLength, newSourceLength: sourceToSet.size(),
|
|
139
|
-
method: applyResult.method,
|
|
140
|
-
message: `Script source updated successfully (${applyResult.method === "UpdateSourceAsync" ? "editor-safe" : "direct assignment"})`,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const [replaceSuccess, replaceResult] = pcall(() => {
|
|
145
|
-
const parent = instance.Parent;
|
|
146
|
-
const name = instance.Name;
|
|
147
|
-
const className = instance.ClassName;
|
|
148
|
-
const wasBaseScript = instance.IsA("BaseScript");
|
|
149
|
-
const enabled = wasBaseScript ? instance.Enabled : undefined;
|
|
150
|
-
|
|
151
|
-
const newScript = new Instance(className as keyof CreatableInstances) as LuaSourceContainer;
|
|
152
|
-
newScript.Name = name;
|
|
153
|
-
// @rbxts/types does not expose PluginSecurity Source writes.
|
|
154
|
-
const writableNewScript = newScript as unknown as { Source: string };
|
|
155
|
-
writableNewScript.Source = sourceToSet;
|
|
156
|
-
if (readScriptSource(newScript) !== sourceToSet) {
|
|
157
|
-
error("Replacement script source did not match the requested source");
|
|
158
|
-
}
|
|
159
|
-
if (wasBaseScript && enabled !== undefined) {
|
|
160
|
-
const newBaseScript = newScript as BaseScript;
|
|
161
|
-
newBaseScript.Enabled = enabled;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
newScript.Parent = parent;
|
|
165
|
-
instance.Destroy();
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
success: true,
|
|
169
|
-
instancePath: getInstancePath(newScript),
|
|
170
|
-
method: "replace",
|
|
171
|
-
message: "Script replaced successfully with new source",
|
|
172
|
-
};
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
if (replaceSuccess) {
|
|
176
|
-
finishRecording(recordingId, true);
|
|
177
|
-
return replaceResult;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
finishRecording(recordingId, false);
|
|
181
|
-
return {
|
|
182
|
-
error: `Failed to set script source. ${applyResult.error} Replace method failed: ${replaceResult}`,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function editScriptLines(requestData: Record<string, unknown>) {
|
|
187
|
-
const instancePath = requestData.instancePath as string;
|
|
188
|
-
let oldString = requestData.old_string as string;
|
|
189
|
-
let newString = requestData.new_string as string;
|
|
190
|
-
const startLine = requestData.startLine as number | undefined;
|
|
191
|
-
|
|
192
|
-
if (!instancePath || oldString === undefined || newString === undefined) {
|
|
193
|
-
return { error: "Instance path, old_string, and new_string are required" };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
oldString = normalizeEscapes(oldString);
|
|
197
|
-
newString = normalizeEscapes(newString);
|
|
198
|
-
|
|
199
|
-
const instance = getInstanceByPath(instancePath);
|
|
200
|
-
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
201
|
-
if (!instance.IsA("LuaSourceContainer")) {
|
|
202
|
-
return { error: `Instance is not a script-like object: ${instance.ClassName}` };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const recordingId = beginRecording(`Edit script: ${instance.Name}`);
|
|
206
|
-
|
|
207
|
-
const [success, result] = pcall(() => {
|
|
208
|
-
const source = readScriptSource(instance);
|
|
209
|
-
const searchLen = oldString.size();
|
|
210
|
-
let matchStart: number;
|
|
211
|
-
|
|
212
|
-
if (startLine !== undefined) {
|
|
213
|
-
if (startLine < 1) error(`startLine must be >= 1 (got ${startLine})`);
|
|
214
|
-
|
|
215
|
-
let lineStartByte = 1;
|
|
216
|
-
let currentLine = 1;
|
|
217
|
-
while (currentLine < startLine) {
|
|
218
|
-
const [nlPos] = string.find(source, "\n", lineStartByte, true);
|
|
219
|
-
if (nlPos === undefined) {
|
|
220
|
-
error(`startLine ${startLine} is past end of script (${currentLine} lines)`);
|
|
221
|
-
}
|
|
222
|
-
lineStartByte = (nlPos as number) + 1;
|
|
223
|
-
currentLine++;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const candidate = string.sub(source, lineStartByte, lineStartByte + searchLen - 1);
|
|
227
|
-
if (candidate !== oldString) {
|
|
228
|
-
error(`old_string does not match at line ${startLine}. Use get_script_source to verify the exact text at that line.`);
|
|
229
|
-
}
|
|
230
|
-
matchStart = lineStartByte;
|
|
231
|
-
} else {
|
|
232
|
-
let count = 0;
|
|
233
|
-
let searchPos = 1;
|
|
234
|
-
let firstMatch: number | undefined;
|
|
235
|
-
while (true) {
|
|
236
|
-
const [foundStart] = string.find(source, oldString, searchPos, true);
|
|
237
|
-
if (foundStart === undefined) break;
|
|
238
|
-
if (firstMatch === undefined) firstMatch = foundStart;
|
|
239
|
-
count++;
|
|
240
|
-
if (count > 1) break;
|
|
241
|
-
searchPos = foundStart + searchLen;
|
|
242
|
-
}
|
|
243
|
-
if (count === 0) error("old_string not found in script. If old_string contains repeated patterns (e.g. closing braces), pass startLine to anchor the edit.");
|
|
244
|
-
if (count > 1) error("old_string matches multiple locations. Provide more surrounding context, or pass startLine to anchor the edit to a specific line.");
|
|
245
|
-
matchStart = firstMatch as number;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Byte-slice replacement avoids Lua pattern escaping (safe for multi-byte chars like em dashes).
|
|
249
|
-
const newSource = string.sub(source, 1, matchStart - 1) + newString + string.sub(source, matchStart + searchLen);
|
|
250
|
-
|
|
251
|
-
const applyResult = applyScriptSource(instance, newSource, source);
|
|
252
|
-
if (!applyResult.success) error(applyResult.error);
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
success: true,
|
|
256
|
-
instancePath,
|
|
257
|
-
method: applyResult.method,
|
|
258
|
-
message: "Script edited successfully",
|
|
259
|
-
};
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
if (success) {
|
|
263
|
-
finishRecording(recordingId, true);
|
|
264
|
-
return result;
|
|
265
|
-
}
|
|
266
|
-
finishRecording(recordingId, false);
|
|
267
|
-
return { error: `Failed to edit script: ${result}` };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function insertScriptLines(requestData: Record<string, unknown>) {
|
|
271
|
-
const instancePath = requestData.instancePath as string;
|
|
272
|
-
const afterLine = (requestData.afterLine as number) ?? 0;
|
|
273
|
-
let newContent = requestData.newContent as string;
|
|
274
|
-
|
|
275
|
-
if (!instancePath || !newContent) return { error: "Instance path and newContent are required" };
|
|
276
|
-
|
|
277
|
-
newContent = normalizeEscapes(newContent);
|
|
278
|
-
|
|
279
|
-
const instance = getInstanceByPath(instancePath);
|
|
280
|
-
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
281
|
-
if (!instance.IsA("LuaSourceContainer")) {
|
|
282
|
-
return { error: `Instance is not a script-like object: ${instance.ClassName}` };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const recordingId = beginRecording(`Insert script lines after line ${afterLine}: ${instance.Name}`);
|
|
286
|
-
|
|
287
|
-
const [success, result] = pcall(() => {
|
|
288
|
-
const source = readScriptSource(instance);
|
|
289
|
-
const [lines, hadTrailingNewline] = splitLines(source);
|
|
290
|
-
const totalLines = lines.size();
|
|
291
|
-
|
|
292
|
-
if (afterLine < 0 || afterLine > totalLines) error(`afterLine out of range (0-${totalLines})`);
|
|
293
|
-
|
|
294
|
-
const [newLines] = splitLines(newContent);
|
|
295
|
-
const resultLines: string[] = [];
|
|
296
|
-
|
|
297
|
-
for (let i = 0; i < afterLine; i++) resultLines.push(lines[i]);
|
|
298
|
-
for (const line of newLines) resultLines.push(line);
|
|
299
|
-
for (let i = afterLine; i < totalLines; i++) resultLines.push(lines[i]);
|
|
300
|
-
|
|
301
|
-
const newSource = joinLines(resultLines, hadTrailingNewline);
|
|
302
|
-
const applyResult = applyScriptSource(instance, newSource, source);
|
|
303
|
-
if (!applyResult.success) error(applyResult.error);
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
success: true, instancePath,
|
|
307
|
-
insertedAfterLine: afterLine,
|
|
308
|
-
linesInserted: newLines.size(),
|
|
309
|
-
newLineCount: resultLines.size(),
|
|
310
|
-
method: applyResult.method,
|
|
311
|
-
message: "Script lines inserted successfully",
|
|
312
|
-
};
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
if (success) {
|
|
316
|
-
finishRecording(recordingId, true);
|
|
317
|
-
return result;
|
|
318
|
-
}
|
|
319
|
-
finishRecording(recordingId, false);
|
|
320
|
-
return { error: `Failed to insert script lines: ${result}` };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function deleteScriptLines(requestData: Record<string, unknown>) {
|
|
324
|
-
const instancePath = requestData.instancePath as string;
|
|
325
|
-
const startLine = requestData.startLine as number;
|
|
326
|
-
const endLine = requestData.endLine as number;
|
|
327
|
-
|
|
328
|
-
if (!instancePath || !startLine || !endLine) {
|
|
329
|
-
return { error: "Instance path, startLine, and endLine are required" };
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const instance = getInstanceByPath(instancePath);
|
|
333
|
-
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
334
|
-
if (!instance.IsA("LuaSourceContainer")) {
|
|
335
|
-
return { error: `Instance is not a script-like object: ${instance.ClassName}` };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const recordingId = beginRecording(`Delete script lines ${startLine}-${endLine}: ${instance.Name}`);
|
|
339
|
-
|
|
340
|
-
const [success, result] = pcall(() => {
|
|
341
|
-
const source = readScriptSource(instance);
|
|
342
|
-
const [lines, hadTrailingNewline] = splitLines(source);
|
|
343
|
-
const totalLines = lines.size();
|
|
344
|
-
|
|
345
|
-
if (startLine < 1 || startLine > totalLines) error(`startLine out of range (1-${totalLines})`);
|
|
346
|
-
if (endLine < startLine || endLine > totalLines) error(`endLine out of range (${startLine}-${totalLines})`);
|
|
347
|
-
|
|
348
|
-
const resultLines: string[] = [];
|
|
349
|
-
for (let i = 0; i < startLine - 1; i++) resultLines.push(lines[i]);
|
|
350
|
-
for (let i = endLine; i < totalLines; i++) resultLines.push(lines[i]);
|
|
351
|
-
|
|
352
|
-
const newSource = joinLines(resultLines, hadTrailingNewline);
|
|
353
|
-
const applyResult = applyScriptSource(instance, newSource, source);
|
|
354
|
-
if (!applyResult.success) error(applyResult.error);
|
|
355
|
-
|
|
356
|
-
return {
|
|
357
|
-
success: true, instancePath,
|
|
358
|
-
deletedLines: { startLine, endLine },
|
|
359
|
-
linesDeleted: endLine - startLine + 1,
|
|
360
|
-
newLineCount: resultLines.size(),
|
|
361
|
-
method: applyResult.method,
|
|
362
|
-
message: "Script lines deleted successfully",
|
|
363
|
-
};
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
if (success) {
|
|
367
|
-
finishRecording(recordingId, true);
|
|
368
|
-
return result;
|
|
369
|
-
}
|
|
370
|
-
finishRecording(recordingId, false);
|
|
371
|
-
return { error: `Failed to delete script lines: ${result}` };
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function escapeLuaPattern(s: string): string {
|
|
375
|
-
return s.gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")[0];
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function escapeLuaReplacement(s: string): string {
|
|
379
|
-
return s.gsub("%%", "%%%%")[0];
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function caseInsensitiveLiteralReplace(src: string, searchStr: string, repl: string): [string, number] {
|
|
383
|
-
const lowerSrc = src.lower();
|
|
384
|
-
const lowerSearch = searchStr.lower();
|
|
385
|
-
const parts: string[] = [];
|
|
386
|
-
let lastEnd = 1;
|
|
387
|
-
const searchLen = lowerSearch.size();
|
|
388
|
-
let pos = 1;
|
|
389
|
-
let replCount = 0;
|
|
390
|
-
|
|
391
|
-
while (true) {
|
|
392
|
-
const [foundStart] = string.find(lowerSrc, lowerSearch, pos, true);
|
|
393
|
-
if (foundStart === undefined) break;
|
|
394
|
-
parts.push(string.sub(src, lastEnd, foundStart - 1));
|
|
395
|
-
parts.push(repl);
|
|
396
|
-
lastEnd = foundStart + searchLen;
|
|
397
|
-
pos = foundStart + searchLen;
|
|
398
|
-
replCount++;
|
|
399
|
-
}
|
|
400
|
-
parts.push(string.sub(src, lastEnd));
|
|
401
|
-
return [parts.join(""), replCount];
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function findAndReplaceInScripts(requestData: Record<string, unknown>) {
|
|
405
|
-
const searchPattern = requestData.pattern as string;
|
|
406
|
-
const replacement = requestData.replacement as string;
|
|
407
|
-
|
|
408
|
-
if (!searchPattern) return { error: "pattern is required" };
|
|
409
|
-
if (replacement === undefined) return { error: "replacement is required" };
|
|
410
|
-
|
|
411
|
-
const caseSensitive = (requestData.caseSensitive as boolean) ?? false;
|
|
412
|
-
const usePattern = (requestData.usePattern as boolean) ?? false;
|
|
413
|
-
const searchPath = (requestData.path as string) ?? "";
|
|
414
|
-
const classFilter = requestData.classFilter as string | undefined;
|
|
415
|
-
const dryRun = (requestData.dryRun as boolean) ?? false;
|
|
416
|
-
const maxReplacements = (requestData.maxReplacements as number) ?? 1000;
|
|
417
|
-
|
|
418
|
-
if (!caseSensitive && usePattern) {
|
|
419
|
-
return { error: "Case-insensitive Lua pattern replacement is not supported. Use caseSensitive: true with usePattern: true, or use literal matching." };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const startInstance = searchPath !== "" ? getInstanceByPath(searchPath) : game;
|
|
423
|
-
if (!startInstance) return { error: `Path not found: ${searchPath}` };
|
|
424
|
-
|
|
425
|
-
interface ScriptChange {
|
|
426
|
-
instancePath: string;
|
|
427
|
-
name: string;
|
|
428
|
-
className: string;
|
|
429
|
-
replacements: number;
|
|
430
|
-
error?: string;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const changes: ScriptChange[] = [];
|
|
434
|
-
let totalReplacements = 0;
|
|
435
|
-
let scriptsSearched = 0;
|
|
436
|
-
let hitLimit = false;
|
|
437
|
-
|
|
438
|
-
const recordingId = dryRun ? undefined : beginRecording("Find and replace in scripts");
|
|
439
|
-
|
|
440
|
-
function processInstance(instance: Instance) {
|
|
441
|
-
if (hitLimit) return;
|
|
442
|
-
|
|
443
|
-
const matchesClass = classFilter === undefined
|
|
444
|
-
|| instance.ClassName.lower().find(classFilter.lower())[0] !== undefined;
|
|
445
|
-
if (instance.IsA("LuaSourceContainer") && matchesClass) {
|
|
446
|
-
scriptsSearched++;
|
|
447
|
-
const source = readScriptSource(instance);
|
|
448
|
-
|
|
449
|
-
let newSource: string;
|
|
450
|
-
let replCount: number;
|
|
451
|
-
|
|
452
|
-
if (usePattern) {
|
|
453
|
-
const [result, count] = string.gsub(source, searchPattern, replacement);
|
|
454
|
-
newSource = result;
|
|
455
|
-
replCount = count;
|
|
456
|
-
} else if (caseSensitive) {
|
|
457
|
-
const escaped = escapeLuaPattern(searchPattern);
|
|
458
|
-
const escapedRepl = escapeLuaReplacement(replacement);
|
|
459
|
-
const [result, count] = string.gsub(source, escaped, escapedRepl);
|
|
460
|
-
newSource = result;
|
|
461
|
-
replCount = count;
|
|
462
|
-
} else {
|
|
463
|
-
[newSource, replCount] = caseInsensitiveLiteralReplace(source, searchPattern, replacement);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (replCount > 0) {
|
|
467
|
-
if (totalReplacements + replCount > maxReplacements) {
|
|
468
|
-
hitLimit = true;
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const applyResult = dryRun
|
|
473
|
-
? undefined
|
|
474
|
-
: applyScriptSource(instance, newSource, source);
|
|
475
|
-
if (applyResult !== undefined && !applyResult.success) {
|
|
476
|
-
changes.push({
|
|
477
|
-
instancePath: getInstancePath(instance),
|
|
478
|
-
name: instance.Name,
|
|
479
|
-
className: instance.ClassName,
|
|
480
|
-
replacements: 0,
|
|
481
|
-
error: applyResult.error ?? "Script write failed verification",
|
|
482
|
-
});
|
|
483
|
-
} else {
|
|
484
|
-
totalReplacements += replCount;
|
|
485
|
-
changes.push({
|
|
486
|
-
instancePath: getInstancePath(instance),
|
|
487
|
-
name: instance.Name,
|
|
488
|
-
className: instance.ClassName,
|
|
489
|
-
replacements: replCount,
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
for (const child of instance.GetChildren()) {
|
|
496
|
-
if (hitLimit) return;
|
|
497
|
-
processInstance(child);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const [traversalSuccess, traversalResult] = pcall(() => processInstance(startInstance));
|
|
502
|
-
|
|
503
|
-
const failedScripts = changes.filter((change) => change.error !== undefined).size();
|
|
504
|
-
const scriptsModified = changes.size() - failedScripts;
|
|
505
|
-
if (recordingId !== undefined) {
|
|
506
|
-
finishRecording(recordingId, scriptsModified > 0);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
return {
|
|
510
|
-
success: traversalSuccess && failedScripts === 0,
|
|
511
|
-
error: traversalSuccess ? undefined : `Script traversal failed: ${traversalResult}`,
|
|
512
|
-
dryRun,
|
|
513
|
-
pattern: searchPattern,
|
|
514
|
-
replacement,
|
|
515
|
-
totalReplacements,
|
|
516
|
-
scriptsSearched,
|
|
517
|
-
scriptsModified,
|
|
518
|
-
scriptsFailed: failedScripts,
|
|
519
|
-
changes,
|
|
520
|
-
truncated: hitLimit,
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
export = {
|
|
525
|
-
getScriptSource,
|
|
526
|
-
setScriptSource,
|
|
527
|
-
editScriptLines,
|
|
528
|
-
insertScriptLines,
|
|
529
|
-
deleteScriptLines,
|
|
530
|
-
findAndReplaceInScripts,
|
|
531
|
-
};
|