@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,460 +0,0 @@
|
|
|
1
|
-
import Utils from "../Utils";
|
|
2
|
-
|
|
3
|
-
const { getInstanceByPath } = Utils;
|
|
4
|
-
|
|
5
|
-
const HttpService = game.GetService("HttpService");
|
|
6
|
-
const RunService = game.GetService("RunService");
|
|
7
|
-
const ServerStorage = game.GetService("ServerStorage");
|
|
8
|
-
|
|
9
|
-
const LOG_PREFIX = "Breakpoint";
|
|
10
|
-
const REGISTRY_KEY_PREFIX = "MCP_BREAKPOINTS_V1_";
|
|
11
|
-
const MCP_PLACE_ID_ATTRIBUTE = "__MCPPlaceId";
|
|
12
|
-
|
|
13
|
-
let pluginRef: Plugin | undefined;
|
|
14
|
-
let loadedRegistryKey: string | undefined;
|
|
15
|
-
let loadedRegistryFromSettings = false;
|
|
16
|
-
|
|
17
|
-
interface ScriptBreakpointSpec {
|
|
18
|
-
Line: number;
|
|
19
|
-
Enabled?: boolean;
|
|
20
|
-
Condition?: string;
|
|
21
|
-
LogMessage?: string;
|
|
22
|
-
ContinueExecution?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface ScriptBreakpointResult {
|
|
26
|
-
Verified?: boolean;
|
|
27
|
-
Line?: number;
|
|
28
|
-
Message?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface ScriptDebuggerServiceLike extends Instance {
|
|
32
|
-
AddBreakpoint(this: ScriptDebuggerServiceLike, script: Instance, breakpoint: ScriptBreakpointSpec): ScriptBreakpointResult;
|
|
33
|
-
RemoveBreakpoint(this: ScriptDebuggerServiceLike, script: Instance, line: number): boolean;
|
|
34
|
-
ClearBreakpoints(this: ScriptDebuggerServiceLike): void;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface BreakpointEntry {
|
|
38
|
-
script_path: string;
|
|
39
|
-
line: number;
|
|
40
|
-
requested_line?: number;
|
|
41
|
-
enabled?: boolean;
|
|
42
|
-
condition?: string;
|
|
43
|
-
log_message?: string;
|
|
44
|
-
continue_execution?: boolean;
|
|
45
|
-
verified?: false;
|
|
46
|
-
message?: string;
|
|
47
|
-
created_at?: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface PersistedBreakpointEntry {
|
|
51
|
-
script_path: string;
|
|
52
|
-
line: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface RegistryScope {
|
|
56
|
-
key: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const breakpoints = new Map<string, BreakpointEntry>();
|
|
60
|
-
|
|
61
|
-
function init(p: Plugin): void {
|
|
62
|
-
pluginRef = p;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function breakpointKey(scriptPath: string, line: number): string {
|
|
66
|
-
return `${scriptPath}:${line}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function computeInstanceId(): string {
|
|
70
|
-
if (game.PlaceId !== 0) {
|
|
71
|
-
return `place:${tostring(game.PlaceId)}`;
|
|
72
|
-
}
|
|
73
|
-
const existing = ServerStorage.GetAttribute(MCP_PLACE_ID_ATTRIBUTE);
|
|
74
|
-
if (typeIs(existing, "string") && existing !== "") {
|
|
75
|
-
return `anon:${existing as string}`;
|
|
76
|
-
}
|
|
77
|
-
const fresh = HttpService.GenerateGUID(false);
|
|
78
|
-
pcall(() => ServerStorage.SetAttribute(MCP_PLACE_ID_ATTRIBUTE, fresh));
|
|
79
|
-
return `anon:${fresh}`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function detectRole(): string {
|
|
83
|
-
if (!RunService.IsRunning()) return "edit";
|
|
84
|
-
if (RunService.IsServer()) return "server";
|
|
85
|
-
return "client";
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function requestedRole(requestData: Record<string, unknown>): string {
|
|
89
|
-
return typeIs(requestData.__mcp_target_role, "string") && requestData.__mcp_target_role !== ""
|
|
90
|
-
? requestData.__mcp_target_role as string
|
|
91
|
-
: detectRole();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function registryScope(requestData: Record<string, unknown>): RegistryScope {
|
|
95
|
-
const instanceId = typeIs(requestData.__mcp_instance_id, "string") && requestData.__mcp_instance_id !== ""
|
|
96
|
-
? requestData.__mcp_instance_id as string
|
|
97
|
-
: computeInstanceId();
|
|
98
|
-
const role = requestedRole(requestData);
|
|
99
|
-
return {
|
|
100
|
-
key: `${REGISTRY_KEY_PREFIX}${instanceId}:${role}`,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function readSetting(key: string): unknown {
|
|
105
|
-
if (!pluginRef) return undefined;
|
|
106
|
-
const [ok, value] = pcall(() => pluginRef!.GetSetting(key));
|
|
107
|
-
return ok ? value : undefined;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function writeSetting(key: string, value: unknown): boolean {
|
|
111
|
-
if (!pluginRef) return false;
|
|
112
|
-
const [ok] = pcall(() => pluginRef!.SetSetting(key, value));
|
|
113
|
-
return ok;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function decodePersistedBreakpointEntry(value: unknown): BreakpointEntry | undefined {
|
|
117
|
-
if (!typeIs(value, "table")) return undefined;
|
|
118
|
-
const data = value as Record<string, unknown>;
|
|
119
|
-
if (!typeIs(data.script_path, "string") || data.script_path === "") return undefined;
|
|
120
|
-
if (!typeIs(data.line, "number") || data.line < 1) return undefined;
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
script_path: data.script_path as string,
|
|
124
|
-
line: math.floor(data.line as number),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function loadRegistry(requestData: Record<string, unknown>): RegistryScope {
|
|
129
|
-
const scope = registryScope(requestData);
|
|
130
|
-
if (loadedRegistryKey !== scope.key) {
|
|
131
|
-
breakpoints.clear();
|
|
132
|
-
loadedRegistryKey = scope.key;
|
|
133
|
-
loadedRegistryFromSettings = false;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (loadedRegistryFromSettings) return scope;
|
|
137
|
-
loadedRegistryFromSettings = true;
|
|
138
|
-
|
|
139
|
-
const stored = readSetting(scope.key);
|
|
140
|
-
if (stored === undefined) return scope;
|
|
141
|
-
|
|
142
|
-
let decoded: unknown = stored;
|
|
143
|
-
if (typeIs(stored, "string")) {
|
|
144
|
-
const [ok, result] = pcall(() => HttpService.JSONDecode(stored as string));
|
|
145
|
-
if (!ok) return scope;
|
|
146
|
-
decoded = result;
|
|
147
|
-
}
|
|
148
|
-
if (!typeIs(decoded, "table")) return scope;
|
|
149
|
-
|
|
150
|
-
breakpoints.clear();
|
|
151
|
-
for (const item of decoded as unknown[]) {
|
|
152
|
-
const entry = decodePersistedBreakpointEntry(item);
|
|
153
|
-
if (entry) {
|
|
154
|
-
breakpoints.set(breakpointKey(entry.script_path, entry.line), entry);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return scope;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
interface PersistResult {
|
|
161
|
-
ok: boolean;
|
|
162
|
-
error?: string;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function persistRegistry(scope: RegistryScope): PersistResult {
|
|
166
|
-
if (!pluginRef) return { ok: false, error: "Plugin settings are unavailable; managed breakpoint registry is memory-only." };
|
|
167
|
-
|
|
168
|
-
const out: PersistedBreakpointEntry[] = [];
|
|
169
|
-
for (const [, entry] of breakpoints) {
|
|
170
|
-
out.push({
|
|
171
|
-
script_path: entry.script_path,
|
|
172
|
-
line: entry.line,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const [encodedOk, encoded] = pcall(() => HttpService.JSONEncode(out));
|
|
177
|
-
if (!encodedOk || !typeIs(encoded, "string")) {
|
|
178
|
-
return { ok: false, error: `Failed to encode managed breakpoint registry: ${tostring(encoded)}` };
|
|
179
|
-
}
|
|
180
|
-
if (!writeSetting(scope.key, encoded)) {
|
|
181
|
-
return { ok: false, error: "Failed to persist managed breakpoint registry with plugin:SetSetting." };
|
|
182
|
-
}
|
|
183
|
-
const stored = readSetting(scope.key);
|
|
184
|
-
if (stored !== encoded) {
|
|
185
|
-
return { ok: false, error: "Failed to verify managed breakpoint registry persistence after plugin:SetSetting." };
|
|
186
|
-
}
|
|
187
|
-
return { ok: true };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function attachPersistenceWarning(response: Record<string, unknown>, persist: PersistResult): Record<string, unknown> {
|
|
191
|
-
if (!persist.ok) {
|
|
192
|
-
response.managed_registry_persisted = false;
|
|
193
|
-
response.registry_error = persist.error;
|
|
194
|
-
}
|
|
195
|
-
return response;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function serviceError(message?: string): Record<string, unknown> {
|
|
199
|
-
return {
|
|
200
|
-
error: "script_debugger_unavailable",
|
|
201
|
-
message: message ?? "ScriptDebuggerService is unavailable. Enable the Studio Debugger Luau API beta feature and restart Studio.",
|
|
202
|
-
betaFeatureRequired: true,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function operationError(errorCode: string, operation: string, raw: unknown): Record<string, unknown> {
|
|
207
|
-
return {
|
|
208
|
-
error: errorCode,
|
|
209
|
-
message:
|
|
210
|
-
`${operation} failed. The breakpoints tool requires the Studio Debugger Luau API beta feature. ` +
|
|
211
|
-
"Enable it in Studio Beta Features and restart/reload Studio, then retry.",
|
|
212
|
-
rawMessage: tostring(raw),
|
|
213
|
-
betaFeatureRequired: true,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function getService(): ScriptDebuggerServiceLike | Record<string, unknown> {
|
|
218
|
-
const provider = game as unknown as { GetService(serviceName: string): Instance };
|
|
219
|
-
const [ok, service] = pcall(() => provider.GetService("ScriptDebuggerService") as ScriptDebuggerServiceLike);
|
|
220
|
-
if (!ok || !service) {
|
|
221
|
-
return serviceError(`ScriptDebuggerService unavailable: ${tostring(service)}`);
|
|
222
|
-
}
|
|
223
|
-
return service;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function luauStringLiteral(value: string): string {
|
|
227
|
-
let escaped = value.gsub("\\", "\\\\")[0];
|
|
228
|
-
escaped = escaped.gsub("\n", "\\n")[0];
|
|
229
|
-
escaped = escaped.gsub("\r", "\\r")[0];
|
|
230
|
-
escaped = escaped.gsub("\t", "\\t")[0];
|
|
231
|
-
escaped = escaped.gsub('"', '\\"')[0];
|
|
232
|
-
return `"${escaped}"`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function buildLogMessage(scriptPath: string, line: number, logMessage: string | undefined): string {
|
|
236
|
-
const prefix = [
|
|
237
|
-
luauStringLiteral(LOG_PREFIX),
|
|
238
|
-
luauStringLiteral(`${scriptPath}:${line}`),
|
|
239
|
-
];
|
|
240
|
-
if (typeIs(logMessage, "string") && logMessage !== "") {
|
|
241
|
-
prefix.push(logMessage);
|
|
242
|
-
}
|
|
243
|
-
return prefix.join(", ");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function listBreakpoints(requestData: Record<string, unknown>): Record<string, unknown> {
|
|
247
|
-
loadRegistry(requestData);
|
|
248
|
-
const out: BreakpointEntry[] = [];
|
|
249
|
-
for (const [, entry] of breakpoints) {
|
|
250
|
-
out.push(entry);
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
breakpoints: out,
|
|
254
|
-
count: out.size(),
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function setBreakpoint(requestData: Record<string, unknown>): unknown {
|
|
259
|
-
const scope = loadRegistry(requestData);
|
|
260
|
-
const serviceOrError = getService();
|
|
261
|
-
if (!serviceOrError.IsA) return serviceOrError;
|
|
262
|
-
const service = serviceOrError as ScriptDebuggerServiceLike;
|
|
263
|
-
|
|
264
|
-
const scriptPath = requestData.script_path as string | undefined;
|
|
265
|
-
const lineRaw = requestData.line as number | undefined;
|
|
266
|
-
if (!typeIs(scriptPath, "string") || scriptPath === "" || !typeIs(lineRaw, "number")) {
|
|
267
|
-
return { error: "invalid_args", message: "breakpoints action=set requires script_path and line" };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const requestedLine = math.floor(lineRaw);
|
|
271
|
-
if (requestedLine < 1) {
|
|
272
|
-
return { error: "invalid_line", message: "line must be a 1-based positive number" };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const instance = getInstanceByPath(scriptPath);
|
|
276
|
-
if (!instance) return { error: "script_not_found", script_path: scriptPath };
|
|
277
|
-
if (!instance.IsA("LuaSourceContainer")) {
|
|
278
|
-
return {
|
|
279
|
-
error: "not_a_script",
|
|
280
|
-
message: `${scriptPath} is ${instance.ClassName}, not a LuaSourceContainer`,
|
|
281
|
-
script_path: scriptPath,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const rawLogMessage = typeIs(requestData.log_message, "string") ? requestData.log_message as string : undefined;
|
|
286
|
-
const hasLogMessage = rawLogMessage !== undefined && rawLogMessage !== "";
|
|
287
|
-
const continueExecution = typeIs(requestData.continue_execution, "boolean")
|
|
288
|
-
? requestData.continue_execution as boolean
|
|
289
|
-
: hasLogMessage;
|
|
290
|
-
const enabled = typeIs(requestData.enabled, "boolean") ? requestData.enabled as boolean : true;
|
|
291
|
-
const effectiveLogMessage = hasLogMessage || continueExecution ? buildLogMessage(scriptPath, requestedLine, rawLogMessage) : undefined;
|
|
292
|
-
|
|
293
|
-
const spec: ScriptBreakpointSpec = {
|
|
294
|
-
Line: requestedLine,
|
|
295
|
-
Enabled: enabled,
|
|
296
|
-
ContinueExecution: continueExecution,
|
|
297
|
-
};
|
|
298
|
-
if (typeIs(requestData.condition, "string") && requestData.condition !== "") {
|
|
299
|
-
spec.Condition = requestData.condition as string;
|
|
300
|
-
}
|
|
301
|
-
if (effectiveLogMessage !== undefined) {
|
|
302
|
-
spec.LogMessage = effectiveLogMessage;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const [ok, result] = pcall(() => service.AddBreakpoint(instance, spec));
|
|
306
|
-
if (!ok) return operationError("add_breakpoint_failed", "ScriptDebuggerService:AddBreakpoint", result);
|
|
307
|
-
|
|
308
|
-
const breakpointResult = result as ScriptBreakpointResult;
|
|
309
|
-
const actualLine = typeIs(breakpointResult.Line, "number") ? breakpointResult.Line : requestedLine;
|
|
310
|
-
const verified = typeIs(breakpointResult.Verified, "boolean") ? breakpointResult.Verified : undefined;
|
|
311
|
-
const message = typeIs(breakpointResult.Message, "string") ? breakpointResult.Message : undefined;
|
|
312
|
-
const entry: BreakpointEntry = {
|
|
313
|
-
script_path: scriptPath,
|
|
314
|
-
line: actualLine,
|
|
315
|
-
requested_line: actualLine !== requestedLine ? requestedLine : undefined,
|
|
316
|
-
enabled,
|
|
317
|
-
condition: spec.Condition,
|
|
318
|
-
log_message: rawLogMessage,
|
|
319
|
-
continue_execution: continueExecution,
|
|
320
|
-
verified: verified === false ? false : undefined,
|
|
321
|
-
message,
|
|
322
|
-
created_at: DateTime.now().UnixTimestampMillis,
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
breakpoints.set(breakpointKey(scriptPath, actualLine), entry);
|
|
326
|
-
return attachPersistenceWarning({
|
|
327
|
-
ok: true,
|
|
328
|
-
breakpoint: entry,
|
|
329
|
-
}, persistRegistry(scope));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function removeBreakpoint(requestData: Record<string, unknown>): unknown {
|
|
333
|
-
const scope = loadRegistry(requestData);
|
|
334
|
-
const serviceOrError = getService();
|
|
335
|
-
if (!serviceOrError.IsA) return serviceOrError;
|
|
336
|
-
const service = serviceOrError as ScriptDebuggerServiceLike;
|
|
337
|
-
|
|
338
|
-
const scriptPath = requestData.script_path as string | undefined;
|
|
339
|
-
const lineRaw = requestData.line as number | undefined;
|
|
340
|
-
if (!typeIs(scriptPath, "string") || scriptPath === "" || !typeIs(lineRaw, "number")) {
|
|
341
|
-
return { error: "invalid_args", message: "breakpoints action=remove requires script_path and line" };
|
|
342
|
-
}
|
|
343
|
-
const line = math.floor(lineRaw);
|
|
344
|
-
if (line < 1) {
|
|
345
|
-
return { error: "invalid_line", message: "line must be a 1-based positive number" };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const instance = getInstanceByPath(scriptPath);
|
|
349
|
-
if (!instance) return { error: "script_not_found", script_path: scriptPath };
|
|
350
|
-
if (!instance.IsA("LuaSourceContainer")) {
|
|
351
|
-
return {
|
|
352
|
-
error: "not_a_script",
|
|
353
|
-
message: `${scriptPath} is ${instance.ClassName}, not a LuaSourceContainer`,
|
|
354
|
-
script_path: scriptPath,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const [ok, removed] = pcall(() => service.RemoveBreakpoint(instance, line));
|
|
359
|
-
if (!ok) return operationError("remove_breakpoint_failed", "ScriptDebuggerService:RemoveBreakpoint", removed);
|
|
360
|
-
|
|
361
|
-
breakpoints.delete(breakpointKey(scriptPath, line));
|
|
362
|
-
return attachPersistenceWarning({
|
|
363
|
-
ok: true,
|
|
364
|
-
removed,
|
|
365
|
-
script_path: scriptPath,
|
|
366
|
-
line,
|
|
367
|
-
}, persistRegistry(scope));
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function clearManagedBreakpoints(requestData: Record<string, unknown>): unknown {
|
|
371
|
-
const scope = loadRegistry(requestData);
|
|
372
|
-
const serviceOrError = getService();
|
|
373
|
-
if (!serviceOrError.IsA) return serviceOrError;
|
|
374
|
-
const service = serviceOrError as ScriptDebuggerServiceLike;
|
|
375
|
-
|
|
376
|
-
let cleared = 0;
|
|
377
|
-
const errors: Record<string, unknown>[] = [];
|
|
378
|
-
|
|
379
|
-
for (const [key, entry] of breakpoints) {
|
|
380
|
-
const instance = getInstanceByPath(entry.script_path);
|
|
381
|
-
if (!instance || !instance.IsA("LuaSourceContainer")) {
|
|
382
|
-
breakpoints.delete(key);
|
|
383
|
-
cleared += 1;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const [ok, removedOrError] = pcall(() => service.RemoveBreakpoint(instance, entry.line));
|
|
388
|
-
if (ok) {
|
|
389
|
-
breakpoints.delete(key);
|
|
390
|
-
cleared += 1;
|
|
391
|
-
} else {
|
|
392
|
-
errors.push({
|
|
393
|
-
script_path: entry.script_path,
|
|
394
|
-
line: entry.line,
|
|
395
|
-
error: tostring(removedOrError),
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (errors.size() > 0) {
|
|
401
|
-
return {
|
|
402
|
-
ok: false,
|
|
403
|
-
cleared,
|
|
404
|
-
errors,
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return attachPersistenceWarning({
|
|
409
|
-
ok: true,
|
|
410
|
-
cleared,
|
|
411
|
-
}, persistRegistry(scope));
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function clearAllBreakpoints(requestData: Record<string, unknown>): unknown {
|
|
415
|
-
const scope = loadRegistry(requestData);
|
|
416
|
-
const serviceOrError = getService();
|
|
417
|
-
if (!serviceOrError.IsA) return serviceOrError;
|
|
418
|
-
const service = serviceOrError as ScriptDebuggerServiceLike;
|
|
419
|
-
|
|
420
|
-
const managedCount = breakpoints.size();
|
|
421
|
-
const [ok, err] = pcall(() => service.ClearBreakpoints());
|
|
422
|
-
if (!ok) return operationError("clear_breakpoints_failed", "ScriptDebuggerService:ClearBreakpoints", err);
|
|
423
|
-
breakpoints.clear();
|
|
424
|
-
return attachPersistenceWarning({
|
|
425
|
-
ok: true,
|
|
426
|
-
cleared_managed: managedCount,
|
|
427
|
-
}, persistRegistry(scope));
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function clearBreakpoints(requestData: Record<string, unknown>): unknown {
|
|
431
|
-
if (requestData.clear_all === true) {
|
|
432
|
-
return clearAllBreakpoints(requestData);
|
|
433
|
-
}
|
|
434
|
-
return clearManagedBreakpoints(requestData);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function breakpointsTool(requestData: Record<string, unknown>): unknown {
|
|
438
|
-
const action = requestData.action as string | undefined;
|
|
439
|
-
if (!typeIs(action, "string") || action === "") {
|
|
440
|
-
return { error: "invalid_args", message: "breakpoints requires action=set|remove|clear|list" };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
switch (action) {
|
|
444
|
-
case "set":
|
|
445
|
-
return setBreakpoint(requestData);
|
|
446
|
-
case "remove":
|
|
447
|
-
return removeBreakpoint(requestData);
|
|
448
|
-
case "clear":
|
|
449
|
-
return clearBreakpoints(requestData);
|
|
450
|
-
case "list":
|
|
451
|
-
return listBreakpoints(requestData);
|
|
452
|
-
default:
|
|
453
|
-
return {
|
|
454
|
-
error: "unknown_action",
|
|
455
|
-
message: `breakpoints action must be one of: set, remove, clear, list (got ${action})`,
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export = { breakpoints: breakpointsTool, init };
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import * as RenderMonitor from "../RenderMonitor";
|
|
2
|
-
|
|
3
|
-
const CaptureService = game.GetService("CaptureService");
|
|
4
|
-
const AssetService = game.GetService("AssetService");
|
|
5
|
-
|
|
6
|
-
const MAX_TILE_SIZE = 1024;
|
|
7
|
-
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
8
|
-
const PAD_BYTE = string.byte("=")[0];
|
|
9
|
-
|
|
10
|
-
const B64: number[] = [];
|
|
11
|
-
for (let i = 0; i < 64; i++) {
|
|
12
|
-
B64[i] = string.byte(BASE64_CHARS, i + 1)[0];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function encodeBase64(buf: buffer): string {
|
|
16
|
-
const len = buffer.len(buf);
|
|
17
|
-
const fullTriples = math.floor(len / 3);
|
|
18
|
-
const remaining = len - fullTriples * 3;
|
|
19
|
-
const outLen = (fullTriples + (remaining > 0 ? 1 : 0)) * 4;
|
|
20
|
-
const out = buffer.create(outLen);
|
|
21
|
-
|
|
22
|
-
let si = 0;
|
|
23
|
-
let di = 0;
|
|
24
|
-
|
|
25
|
-
for (let t = 0; t < fullTriples; t++) {
|
|
26
|
-
const b0 = buffer.readu8(buf, si);
|
|
27
|
-
const b1 = buffer.readu8(buf, si + 1);
|
|
28
|
-
const b2 = buffer.readu8(buf, si + 2);
|
|
29
|
-
|
|
30
|
-
buffer.writeu8(out, di, B64[bit32.rshift(b0, 2)]);
|
|
31
|
-
buffer.writeu8(out, di + 1, B64[bit32.bor(bit32.lshift(bit32.band(b0, 3), 4), bit32.rshift(b1, 4))]);
|
|
32
|
-
buffer.writeu8(out, di + 2, B64[bit32.bor(bit32.lshift(bit32.band(b1, 15), 2), bit32.rshift(b2, 6))]);
|
|
33
|
-
buffer.writeu8(out, di + 3, B64[bit32.band(b2, 63)]);
|
|
34
|
-
|
|
35
|
-
si += 3;
|
|
36
|
-
di += 4;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (remaining === 2) {
|
|
40
|
-
const b0 = buffer.readu8(buf, si);
|
|
41
|
-
const b1 = buffer.readu8(buf, si + 1);
|
|
42
|
-
buffer.writeu8(out, di, B64[bit32.rshift(b0, 2)]);
|
|
43
|
-
buffer.writeu8(out, di + 1, B64[bit32.bor(bit32.lshift(bit32.band(b0, 3), 4), bit32.rshift(b1, 4))]);
|
|
44
|
-
buffer.writeu8(out, di + 2, B64[bit32.lshift(bit32.band(b1, 15), 2)]);
|
|
45
|
-
buffer.writeu8(out, di + 3, PAD_BYTE);
|
|
46
|
-
} else if (remaining === 1) {
|
|
47
|
-
const b0 = buffer.readu8(buf, si);
|
|
48
|
-
buffer.writeu8(out, di, B64[bit32.rshift(b0, 2)]);
|
|
49
|
-
buffer.writeu8(out, di + 1, B64[bit32.lshift(bit32.band(b0, 3), 4)]);
|
|
50
|
-
buffer.writeu8(out, di + 2, PAD_BYTE);
|
|
51
|
-
buffer.writeu8(out, di + 3, PAD_BYTE);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return buffer.tostring(out);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function readPixelsTiled(img: EditableImage, w: number, h: number): buffer {
|
|
58
|
-
const BYTES_PER_PIXEL = 4;
|
|
59
|
-
const fullBuf = buffer.create(w * h * BYTES_PER_PIXEL);
|
|
60
|
-
const fullRowBytes = w * BYTES_PER_PIXEL;
|
|
61
|
-
|
|
62
|
-
for (let ty = 0; ty < h; ty += MAX_TILE_SIZE) {
|
|
63
|
-
const tileH = math.min(MAX_TILE_SIZE, h - ty);
|
|
64
|
-
for (let tx = 0; tx < w; tx += MAX_TILE_SIZE) {
|
|
65
|
-
const tileW = math.min(MAX_TILE_SIZE, w - tx);
|
|
66
|
-
const tileBuf = img.ReadPixelsBuffer(new Vector2(tx, ty), new Vector2(tileW, tileH));
|
|
67
|
-
const tileRowBytes = tileW * BYTES_PER_PIXEL;
|
|
68
|
-
for (let row = 0; row < tileH; row++) {
|
|
69
|
-
buffer.copy(fullBuf, (ty + row) * fullRowBytes + tx * BYTES_PER_PIXEL, tileBuf, row * tileRowBytes, tileRowBytes);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return fullBuf;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Triggers CaptureService:CaptureScreenshot and waits for the temporary
|
|
77
|
-
// content id. Works in any DM, including the play CLIENT (where reading the
|
|
78
|
-
// pixels back is blocked, but capturing is not). The returned rbxtemp:// id is
|
|
79
|
-
// a process-scoped handle: it can be dereferenced from a DIFFERENT, more
|
|
80
|
-
// privileged DM (the edit DM) — see captureRead.
|
|
81
|
-
function doCaptureScreenshot(): { contentId: string } | { error: string } {
|
|
82
|
-
// Fast-fail with a clear reason if the window isn't rendering — otherwise
|
|
83
|
-
// CaptureScreenshot's callback never fires and we'd block for the full 10s.
|
|
84
|
-
const notRendering = RenderMonitor.notRenderingReason();
|
|
85
|
-
if (notRendering !== undefined) return { error: notRendering };
|
|
86
|
-
|
|
87
|
-
let contentId: string | undefined;
|
|
88
|
-
|
|
89
|
-
CaptureService.CaptureScreenshot((id: string) => {
|
|
90
|
-
contentId = id;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const startTime = tick();
|
|
94
|
-
while (contentId === undefined) {
|
|
95
|
-
if (tick() - startTime > 10) {
|
|
96
|
-
return {
|
|
97
|
-
error: "Screenshot capture timed out (CaptureScreenshot callback never fired). The Studio window is likely minimized or occluded — restore it so the viewport renders. (Known Roblox bug: capture can also fail if the viewport renders a solid color.)",
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
task.wait(0.1);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return { contentId };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Promotes a CaptureScreenshot content id into an EditableImage and reads its
|
|
107
|
-
// RGBA pixels. MUST run in the edit/plugin context: the running game VM lacks
|
|
108
|
-
// the privilege to create an EditableImage from a temporary texture id (errors
|
|
109
|
-
// "cannot currently create editable image from temporary texture id"), while
|
|
110
|
-
// the edit DM can — even for an id captured in the play client DM.
|
|
111
|
-
function readContentToBase64(contentId: string): unknown {
|
|
112
|
-
const [editableOk, editableResult] = pcall(() => {
|
|
113
|
-
return AssetService.CreateEditableImageAsync(Content.fromUri(contentId));
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!editableOk) {
|
|
117
|
-
return {
|
|
118
|
-
error: `Failed to create EditableImage from screenshot. Enable EditableImage API: Game Settings > Security > 'Allow Mesh / Image APIs'. (${tostring(editableResult)})`,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const editableImage = editableResult as EditableImage;
|
|
123
|
-
const imgSize = editableImage.Size;
|
|
124
|
-
const w = math.floor(imgSize.X);
|
|
125
|
-
const h = math.floor(imgSize.Y);
|
|
126
|
-
|
|
127
|
-
const [readOk, pixelBuffer] = pcall(() => {
|
|
128
|
-
return readPixelsTiled(editableImage, w, h);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
editableImage.Destroy();
|
|
132
|
-
|
|
133
|
-
if (!readOk) {
|
|
134
|
-
return { error: `Failed to read pixel data: ${tostring(pixelBuffer)}` };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const base64Data = encodeBase64(pixelBuffer as buffer);
|
|
138
|
-
|
|
139
|
-
return { success: true, width: w, height: h, data: base64Data };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Edit-mode single shot: capture and read back in the same (edit) context.
|
|
143
|
-
function captureScreenshotData(): unknown {
|
|
144
|
-
const cap = doCaptureScreenshot();
|
|
145
|
-
if ("error" in cap) return cap;
|
|
146
|
-
return readContentToBase64(cap.contentId);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function captureScreenshot(): unknown {
|
|
150
|
-
return captureScreenshotData();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Play-mode step 1 (run on the CLIENT): capture only, return the temp id.
|
|
154
|
-
function captureBegin(): unknown {
|
|
155
|
-
return doCaptureScreenshot();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Play-mode step 2 (run on EDIT): read pixels from a temp id captured elsewhere.
|
|
159
|
-
function captureRead(requestData: Record<string, unknown>): unknown {
|
|
160
|
-
const contentId = requestData.contentId as string | undefined;
|
|
161
|
-
if (!contentId) return { error: "contentId is required" };
|
|
162
|
-
return readContentToBase64(contentId);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export = {
|
|
166
|
-
captureScreenshotData,
|
|
167
|
-
captureScreenshot,
|
|
168
|
-
captureBegin,
|
|
169
|
-
captureRead,
|
|
170
|
-
};
|