@chrrxs/robloxstudio-mcp 2.16.3 → 2.17.0
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 +232 -65
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +1100 -168
- package/studio-plugin/MCPPlugin.rbxmx +1100 -168
- package/studio-plugin/src/modules/ClientBroker.ts +10 -0
- package/studio-plugin/src/modules/Communication.ts +4 -2
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +36 -10
- package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +460 -0
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -33
- package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +386 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +14 -44
- package/studio-plugin/src/server/index.server.ts +2 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
const HttpService = game.GetService("HttpService");
|
|
2
|
+
const Players = game.GetService("Players");
|
|
3
|
+
const RunService = game.GetService("RunService");
|
|
4
|
+
|
|
5
|
+
interface ScriptProfilerServiceLike extends Instance {
|
|
6
|
+
ServerStart(this: ScriptProfilerServiceLike, frequency?: number): void;
|
|
7
|
+
ServerStop(this: ScriptProfilerServiceLike): void;
|
|
8
|
+
ServerRequestData(this: ScriptProfilerServiceLike): void;
|
|
9
|
+
ClientStart(this: ScriptProfilerServiceLike, player: Player, frequency?: number): void;
|
|
10
|
+
ClientStop(this: ScriptProfilerServiceLike, player: Player): void;
|
|
11
|
+
ClientRequestData(this: ScriptProfilerServiceLike, player: Player): void;
|
|
12
|
+
OnNewData: RBXScriptSignal<(player: Player | undefined, jsonString: string) => void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CategoryInfo {
|
|
16
|
+
Name?: string;
|
|
17
|
+
NodeId?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FunctionInfo {
|
|
21
|
+
Source?: string;
|
|
22
|
+
Name?: string;
|
|
23
|
+
Line?: number;
|
|
24
|
+
TotalDuration?: number;
|
|
25
|
+
Flags?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ProfilingInfo {
|
|
29
|
+
Version?: number;
|
|
30
|
+
SessionStartTime?: number;
|
|
31
|
+
SessionEndTime?: number;
|
|
32
|
+
Categories?: CategoryInfo[];
|
|
33
|
+
Nodes?: unknown[];
|
|
34
|
+
Functions?: FunctionInfo[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface FunctionRow {
|
|
38
|
+
function_index: number;
|
|
39
|
+
name: string;
|
|
40
|
+
source?: string;
|
|
41
|
+
line?: number;
|
|
42
|
+
total_us: number;
|
|
43
|
+
is_native?: boolean;
|
|
44
|
+
is_plugin?: boolean;
|
|
45
|
+
is_debug_label?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_DURATION_MS = 1000;
|
|
49
|
+
const MIN_DURATION_MS = 100;
|
|
50
|
+
const MAX_DURATION_MS = 15000;
|
|
51
|
+
const DEFAULT_FREQUENCY = 1000;
|
|
52
|
+
const DEFAULT_MAX_FUNCTIONS = 20;
|
|
53
|
+
|
|
54
|
+
function getProfilerService(): ScriptProfilerServiceLike | Record<string, unknown> {
|
|
55
|
+
const provider = game as unknown as { GetService(serviceName: string): Instance };
|
|
56
|
+
const [ok, service] = pcall(() => provider.GetService("ScriptProfilerService") as ScriptProfilerServiceLike);
|
|
57
|
+
if (!ok || !service) {
|
|
58
|
+
return {
|
|
59
|
+
error: "script_profiler_unavailable",
|
|
60
|
+
message: `ScriptProfilerService is unavailable: ${tostring(service)}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return service;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeDurationMs(value: unknown): number {
|
|
67
|
+
if (!typeIs(value, "number")) return DEFAULT_DURATION_MS;
|
|
68
|
+
return math.clamp(math.floor(value), MIN_DURATION_MS, MAX_DURATION_MS);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeFrequency(value: unknown): number {
|
|
72
|
+
if (!typeIs(value, "number")) return DEFAULT_FREQUENCY;
|
|
73
|
+
return math.clamp(math.floor(value), 1, 10000);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeMaxFunctions(value: unknown): number {
|
|
77
|
+
if (!typeIs(value, "number")) return DEFAULT_MAX_FUNCTIONS;
|
|
78
|
+
return math.clamp(math.floor(value), 1, 100);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeMinTotalUs(requestData: Record<string, unknown>): number {
|
|
82
|
+
const value = requestData.min_total_us;
|
|
83
|
+
if (typeIs(value, "number")) return math.max(0, value);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stringContains(haystack: string, needle: string): boolean {
|
|
88
|
+
return string.find(string.lower(haystack), string.lower(needle), 1, true)[0] !== undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function localPlayer(): Player | undefined {
|
|
92
|
+
let player = Players.LocalPlayer;
|
|
93
|
+
const started = tick();
|
|
94
|
+
while (!player && tick() - started < 5) {
|
|
95
|
+
task.wait(0.05);
|
|
96
|
+
player = Players.LocalPlayer;
|
|
97
|
+
}
|
|
98
|
+
return player;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function functionDisplayName(func: FunctionInfo): string {
|
|
102
|
+
if (typeIs(func.Name, "string") && func.Name !== "") return func.Name;
|
|
103
|
+
if (typeIs(func.Source, "string") && func.Source !== "") {
|
|
104
|
+
if (typeIs(func.Line, "number") && func.Line > 0) return `${func.Source}:${func.Line}`;
|
|
105
|
+
return func.Source;
|
|
106
|
+
}
|
|
107
|
+
return "<anonymous>";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function flagsOf(func: FunctionInfo): number {
|
|
111
|
+
return typeIs(func.Flags, "number") ? func.Flags : 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isNativeFunction(func: FunctionInfo): boolean {
|
|
115
|
+
return bit32.band(flagsOf(func), 1) !== 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isPluginFunction(func: FunctionInfo): boolean {
|
|
119
|
+
if (bit32.band(flagsOf(func), 2) !== 0) return true;
|
|
120
|
+
return typeIs(func.Source, "string") && string.find(func.Source, "MCPPlugin", 1, true)[0] !== undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isDebugLabel(func: FunctionInfo, filter: string | undefined): boolean {
|
|
124
|
+
if (filter === undefined) return false;
|
|
125
|
+
if (!typeIs(func.Name, "string") || func.Name === "") return false;
|
|
126
|
+
if (!typeIs(func.Source, "string") || func.Source === "" || func.Source === "[C]" || func.Source === "GC") return false;
|
|
127
|
+
if (func.Line !== undefined && func.Line !== 0) return false;
|
|
128
|
+
if (isNativeFunction(func) || isPluginFunction(func)) return false;
|
|
129
|
+
return stringContains(func.Name, filter) || stringContains(func.Source, filter);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pctOfCapture(row: FunctionRow, durationMs: number): number | undefined {
|
|
133
|
+
const captureUs = durationMs * 1000;
|
|
134
|
+
if (captureUs <= 0) return undefined;
|
|
135
|
+
return math.floor((row.total_us / captureUs) * 10000 + 0.5) / 100;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function compactFunction(row: FunctionRow, rank: number, durationMs: number): Record<string, unknown> {
|
|
139
|
+
const out: Record<string, unknown> = {
|
|
140
|
+
rank,
|
|
141
|
+
function_index: row.function_index,
|
|
142
|
+
name: row.name,
|
|
143
|
+
total_us: math.floor(row.total_us + 0.5),
|
|
144
|
+
};
|
|
145
|
+
const pct = pctOfCapture(row, durationMs);
|
|
146
|
+
if (pct !== undefined) out.pct_of_capture = pct;
|
|
147
|
+
if (row.source !== undefined) out.source = row.source;
|
|
148
|
+
if (row.line !== undefined) out.line = row.line;
|
|
149
|
+
if (row.is_native === true) out.is_native = true;
|
|
150
|
+
if (row.is_plugin === true) out.is_plugin = true;
|
|
151
|
+
if (row.is_debug_label === true) out.is_debug_label = true;
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function summarizeProfile(
|
|
156
|
+
rawJson: string,
|
|
157
|
+
profile: ProfilingInfo,
|
|
158
|
+
requestData: Record<string, unknown>,
|
|
159
|
+
durationMs: number,
|
|
160
|
+
frequency: number,
|
|
161
|
+
eventPlayerName: string | undefined,
|
|
162
|
+
): Record<string, unknown> {
|
|
163
|
+
const funcs = typeIs(profile.Functions, "table") ? profile.Functions : [];
|
|
164
|
+
const nodes = typeIs(profile.Nodes, "table") ? profile.Nodes : [];
|
|
165
|
+
const categories = typeIs(profile.Categories, "table") ? profile.Categories : [];
|
|
166
|
+
const maxFunctions = normalizeMaxFunctions(requestData.max_functions);
|
|
167
|
+
const minTotalUs = normalizeMinTotalUs(requestData);
|
|
168
|
+
const includeNative = requestData.include_native === true;
|
|
169
|
+
const includePlugin = requestData.include_plugin === true;
|
|
170
|
+
const filter = typeIs(requestData.filter, "string") && requestData.filter !== "" ? requestData.filter as string : undefined;
|
|
171
|
+
|
|
172
|
+
const rows: FunctionRow[] = [];
|
|
173
|
+
const debugRows: FunctionRow[] = [];
|
|
174
|
+
let omittedNative = 0;
|
|
175
|
+
let omittedPlugin = 0;
|
|
176
|
+
let omittedBelowThreshold = 0;
|
|
177
|
+
let omittedByFilter = 0;
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < funcs.size(); i++) {
|
|
180
|
+
const func = funcs[i];
|
|
181
|
+
if (!typeIs(func, "table")) continue;
|
|
182
|
+
const info = func as FunctionInfo;
|
|
183
|
+
const totalUs = typeIs(info.TotalDuration, "number") ? info.TotalDuration : 0;
|
|
184
|
+
const name = functionDisplayName(info);
|
|
185
|
+
const row: FunctionRow = {
|
|
186
|
+
function_index: i + 1,
|
|
187
|
+
name,
|
|
188
|
+
source: typeIs(info.Source, "string") ? info.Source : undefined,
|
|
189
|
+
line: typeIs(info.Line, "number") ? info.Line : undefined,
|
|
190
|
+
total_us: totalUs,
|
|
191
|
+
is_native: isNativeFunction(info) ? true : undefined,
|
|
192
|
+
is_plugin: isPluginFunction(info) ? true : undefined,
|
|
193
|
+
is_debug_label: isDebugLabel(info, filter) ? true : undefined,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (!includeNative && row.is_native === true) {
|
|
197
|
+
omittedNative += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!includePlugin && row.is_plugin === true) {
|
|
201
|
+
omittedPlugin += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (totalUs < minTotalUs) {
|
|
205
|
+
omittedBelowThreshold += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (filter !== undefined) {
|
|
209
|
+
const text = `${row.name} ${row.source ?? ""}`;
|
|
210
|
+
if (!stringContains(text, filter)) {
|
|
211
|
+
omittedByFilter += 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
rows.push(row);
|
|
216
|
+
if (row.is_debug_label === true) debugRows.push(row);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
rows.sort((a, b) => a.total_us > b.total_us);
|
|
220
|
+
debugRows.sort((a, b) => a.total_us > b.total_us);
|
|
221
|
+
|
|
222
|
+
const topFunctions: Record<string, unknown>[] = [];
|
|
223
|
+
for (let i = 0; i < math.min(maxFunctions, rows.size()); i++) {
|
|
224
|
+
topFunctions.push(compactFunction(rows[i], i + 1, durationMs));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const debugLabels: Record<string, unknown>[] = [];
|
|
228
|
+
for (let i = 0; i < math.min(maxFunctions, debugRows.size()); i++) {
|
|
229
|
+
debugLabels.push(compactFunction(debugRows[i], i + 1, durationMs));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const categoryNames: string[] = [];
|
|
233
|
+
for (let i = 0; i < categories.size(); i++) {
|
|
234
|
+
const category = categories[i];
|
|
235
|
+
if (typeIs(category, "table")) {
|
|
236
|
+
const name = (category as CategoryInfo).Name;
|
|
237
|
+
if (typeIs(name, "string")) categoryNames.push(name);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const omitted: Record<string, number> = {};
|
|
242
|
+
let hasOmitted = false;
|
|
243
|
+
if (omittedNative > 0) {
|
|
244
|
+
omitted.native = omittedNative;
|
|
245
|
+
hasOmitted = true;
|
|
246
|
+
}
|
|
247
|
+
if (omittedPlugin > 0) {
|
|
248
|
+
omitted.plugin = omittedPlugin;
|
|
249
|
+
hasOmitted = true;
|
|
250
|
+
}
|
|
251
|
+
if (omittedBelowThreshold > 0) {
|
|
252
|
+
omitted.below_min_total_us = omittedBelowThreshold;
|
|
253
|
+
hasOmitted = true;
|
|
254
|
+
}
|
|
255
|
+
if (omittedByFilter > 0) {
|
|
256
|
+
omitted.filtered_out = omittedByFilter;
|
|
257
|
+
hasOmitted = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const result: Record<string, unknown> = {
|
|
261
|
+
ok: true,
|
|
262
|
+
duration_ms: durationMs,
|
|
263
|
+
frequency,
|
|
264
|
+
applied: {
|
|
265
|
+
filter: filter ?? undefined,
|
|
266
|
+
min_total_us: minTotalUs,
|
|
267
|
+
include_native: includeNative,
|
|
268
|
+
include_plugin: includePlugin,
|
|
269
|
+
max_functions: maxFunctions,
|
|
270
|
+
sort: "total_us_desc",
|
|
271
|
+
},
|
|
272
|
+
json_bytes: rawJson.size(),
|
|
273
|
+
counts: {
|
|
274
|
+
categories: categories.size(),
|
|
275
|
+
nodes: nodes.size(),
|
|
276
|
+
functions: funcs.size(),
|
|
277
|
+
},
|
|
278
|
+
top_functions: topFunctions,
|
|
279
|
+
debug_labels: debugLabels,
|
|
280
|
+
};
|
|
281
|
+
if (categoryNames.size() > 0) result.categories = categoryNames;
|
|
282
|
+
if (hasOmitted) result.omitted = omitted;
|
|
283
|
+
if (profile.Version !== undefined) result.version = profile.Version;
|
|
284
|
+
if (profile.SessionStartTime !== undefined || profile.SessionEndTime !== undefined) {
|
|
285
|
+
result.session = {
|
|
286
|
+
start_time: profile.SessionStartTime,
|
|
287
|
+
end_time: profile.SessionEndTime,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (eventPlayerName !== undefined) result.player = eventPlayerName;
|
|
291
|
+
if (requestData.__mcp_include_raw_json === true) result.raw_json = rawJson;
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function captureScriptProfiler(requestData: Record<string, unknown>): unknown {
|
|
296
|
+
if (!RunService.IsRunning()) {
|
|
297
|
+
return {
|
|
298
|
+
error: "runtime_target_required",
|
|
299
|
+
message: "Script profiler capture requires a running playtest target such as target=\"server\" or target=\"client-1\".",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const serviceOrError = getProfilerService();
|
|
304
|
+
if (!serviceOrError.IsA) return serviceOrError;
|
|
305
|
+
const service = serviceOrError as ScriptProfilerServiceLike;
|
|
306
|
+
|
|
307
|
+
const durationMs = normalizeDurationMs(requestData.duration_ms);
|
|
308
|
+
const frequency = normalizeFrequency(requestData.frequency);
|
|
309
|
+
const isServer = RunService.IsServer();
|
|
310
|
+
const isClient = RunService.IsClient() && !isServer;
|
|
311
|
+
const player = isClient ? localPlayer() : undefined;
|
|
312
|
+
if (!isServer && !player) {
|
|
313
|
+
return {
|
|
314
|
+
error: "client_player_unavailable",
|
|
315
|
+
message: "Could not resolve Players.LocalPlayer for client profiling.",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let rawJson: string | undefined;
|
|
320
|
+
let eventPlayerName: string | undefined;
|
|
321
|
+
const connection = service.OnNewData.Connect((playerArg: Player | undefined, jsonString: string) => {
|
|
322
|
+
if (rawJson !== undefined) return;
|
|
323
|
+
rawJson = jsonString;
|
|
324
|
+
if (playerArg) eventPlayerName = playerArg.Name;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const [startOk, startErr] = pcall(() => {
|
|
328
|
+
if (isServer) {
|
|
329
|
+
service.ServerStart(frequency);
|
|
330
|
+
} else {
|
|
331
|
+
service.ClientStart(player!, frequency);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
if (!startOk) {
|
|
335
|
+
connection.Disconnect();
|
|
336
|
+
return {
|
|
337
|
+
error: "script_profiler_start_failed",
|
|
338
|
+
message: tostring(startErr),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
task.wait(durationMs / 1000);
|
|
343
|
+
|
|
344
|
+
const [stopOk, stopErr] = pcall(() => {
|
|
345
|
+
if (isServer) {
|
|
346
|
+
service.ServerStop();
|
|
347
|
+
service.ServerRequestData();
|
|
348
|
+
} else {
|
|
349
|
+
service.ClientStop(player!);
|
|
350
|
+
service.ClientRequestData(player!);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
if (!stopOk) {
|
|
354
|
+
connection.Disconnect();
|
|
355
|
+
return {
|
|
356
|
+
error: "script_profiler_stop_failed",
|
|
357
|
+
message: tostring(stopErr),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const requestedAt = tick();
|
|
362
|
+
while (rawJson === undefined && tick() - requestedAt < 5) {
|
|
363
|
+
task.wait(0.05);
|
|
364
|
+
}
|
|
365
|
+
connection.Disconnect();
|
|
366
|
+
|
|
367
|
+
if (rawJson === undefined) {
|
|
368
|
+
return {
|
|
369
|
+
error: "script_profiler_data_timeout",
|
|
370
|
+
message: "ScriptProfilerService did not emit OnNewData after requesting profiler data.",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const [decodeOk, decoded] = pcall(() => HttpService.JSONDecode(rawJson!));
|
|
375
|
+
if (!decodeOk) {
|
|
376
|
+
return {
|
|
377
|
+
error: "script_profiler_decode_failed",
|
|
378
|
+
message: tostring(decoded),
|
|
379
|
+
json_bytes: rawJson.size(),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return summarizeProfile(rawJson, decoded as ProfilingInfo, requestData, durationMs, frequency, eventPlayerName);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export = { captureScriptProfiler };
|
|
@@ -20,17 +20,8 @@ const ScriptEditorService = game.GetService("ScriptEditorService");
|
|
|
20
20
|
const NAV_SIGNAL = "__MCP_NAV__";
|
|
21
21
|
const NAV_RESULT = "__MCP_NAV_RESULT__";
|
|
22
22
|
|
|
23
|
-
interface OutputEntry {
|
|
24
|
-
message: string;
|
|
25
|
-
messageType: string;
|
|
26
|
-
timestamp: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
23
|
let testRunning = false;
|
|
30
|
-
let
|
|
31
|
-
let logConnection: RBXScriptConnection | undefined;
|
|
32
|
-
let testResult: unknown;
|
|
33
|
-
let testError: string | undefined;
|
|
24
|
+
let navLogConnection: RBXScriptConnection | undefined;
|
|
34
25
|
let stopListenerScript: Script | undefined;
|
|
35
26
|
let navResultCallback: ((json: string) => void) | undefined;
|
|
36
27
|
|
|
@@ -176,6 +167,13 @@ function cleanupStopListener() {
|
|
|
176
167
|
}
|
|
177
168
|
}
|
|
178
169
|
|
|
170
|
+
function disconnectNavLogListener() {
|
|
171
|
+
if (navLogConnection) {
|
|
172
|
+
navLogConnection.Disconnect();
|
|
173
|
+
navLogConnection = undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
179
177
|
function startPlaytest(requestData: Record<string, unknown>) {
|
|
180
178
|
const mode = requestData.mode as string | undefined;
|
|
181
179
|
const numPlayers = requestData.numPlayers as number | undefined;
|
|
@@ -194,10 +192,7 @@ function startPlaytest(requestData: Record<string, unknown>) {
|
|
|
194
192
|
// Reset it so subsequent starts don't hit a false "already running".
|
|
195
193
|
if (testRunning && !RunService.IsRunning()) {
|
|
196
194
|
testRunning = false;
|
|
197
|
-
|
|
198
|
-
logConnection.Disconnect();
|
|
199
|
-
logConnection = undefined;
|
|
200
|
-
}
|
|
195
|
+
disconnectNavLogListener();
|
|
201
196
|
cleanupStopListener();
|
|
202
197
|
// Runtime eval bridges are created by the play server/client plugin
|
|
203
198
|
// peers and disappear with the play DataModels.
|
|
@@ -208,25 +203,16 @@ function startPlaytest(requestData: Record<string, unknown>) {
|
|
|
208
203
|
}
|
|
209
204
|
|
|
210
205
|
testRunning = true;
|
|
211
|
-
outputBuffer = [];
|
|
212
|
-
testResult = undefined;
|
|
213
|
-
testError = undefined;
|
|
214
206
|
|
|
215
207
|
cleanupStopListener();
|
|
208
|
+
disconnectNavLogListener();
|
|
216
209
|
|
|
217
|
-
|
|
218
|
-
if (message.sub(1, NAV_SIGNAL.size()) === NAV_SIGNAL) return;
|
|
210
|
+
navLogConnection = LogService.MessageOut.Connect((message) => {
|
|
219
211
|
if (message.sub(1, NAV_RESULT.size() + 1) === `${NAV_RESULT}:`) {
|
|
220
212
|
if (navResultCallback) {
|
|
221
213
|
navResultCallback(message.sub(NAV_RESULT.size() + 2));
|
|
222
214
|
}
|
|
223
|
-
return;
|
|
224
215
|
}
|
|
225
|
-
outputBuffer.push({
|
|
226
|
-
message,
|
|
227
|
-
messageType: messageType.Name,
|
|
228
|
-
timestamp: tick(),
|
|
229
|
-
});
|
|
230
216
|
});
|
|
231
217
|
|
|
232
218
|
const [injected, injErr] = pcall(() => injectStopListener());
|
|
@@ -242,16 +228,11 @@ function startPlaytest(requestData: Record<string, unknown>) {
|
|
|
242
228
|
return StudioTestService.ExecuteRunModeAsync({});
|
|
243
229
|
});
|
|
244
230
|
|
|
245
|
-
if (ok) {
|
|
246
|
-
|
|
247
|
-
} else {
|
|
248
|
-
testError = tostring(result);
|
|
231
|
+
if (!ok) {
|
|
232
|
+
warn(`[robloxstudio-mcp] Playtest ended with error: ${result}`);
|
|
249
233
|
}
|
|
250
234
|
|
|
251
|
-
|
|
252
|
-
logConnection.Disconnect();
|
|
253
|
-
logConnection = undefined;
|
|
254
|
-
}
|
|
235
|
+
disconnectNavLogListener();
|
|
255
236
|
testRunning = false;
|
|
256
237
|
|
|
257
238
|
cleanupStopListener();
|
|
@@ -324,16 +305,6 @@ function stopPlaytest(_requestData: Record<string, unknown>) {
|
|
|
324
305
|
return { success: true, message: "Playtest stopped." };
|
|
325
306
|
}
|
|
326
307
|
|
|
327
|
-
function getPlaytestOutput(_requestData: Record<string, unknown>) {
|
|
328
|
-
return {
|
|
329
|
-
isRunning: testRunning,
|
|
330
|
-
output: [...outputBuffer],
|
|
331
|
-
outputCount: outputBuffer.size(),
|
|
332
|
-
testResult: testResult !== undefined ? tostring(testResult) : undefined,
|
|
333
|
-
testError,
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
308
|
function multiplayerTestStart(requestData: Record<string, unknown>) {
|
|
338
309
|
if (RunService.IsRunning()) {
|
|
339
310
|
return { error: "multiplayer_test_start must be called on the edit DataModel. Route with target=edit." };
|
|
@@ -546,7 +517,6 @@ function characterNavigation(requestData: Record<string, unknown>) {
|
|
|
546
517
|
export = {
|
|
547
518
|
startPlaytest,
|
|
548
519
|
stopPlaytest,
|
|
549
|
-
getPlaytestOutput,
|
|
550
520
|
multiplayerTestStart,
|
|
551
521
|
multiplayerTestState,
|
|
552
522
|
multiplayerTestAddPlayers,
|
|
@@ -6,6 +6,7 @@ import ServerUrlSettings from "../modules/ServerUrlSettings";
|
|
|
6
6
|
import { cleanupLegacyEditBridges, ensureRuntimeBridgeInstalled } from "../modules/EvalBridges";
|
|
7
7
|
import RuntimeLogBuffer from "../modules/RuntimeLogBuffer";
|
|
8
8
|
import StopPlayMonitor from "../modules/StopPlayMonitor";
|
|
9
|
+
import BreakpointHandlers from "../modules/handlers/BreakpointHandlers";
|
|
9
10
|
import * as RenderMonitor from "../modules/RenderMonitor";
|
|
10
11
|
|
|
11
12
|
// Track render-loop liveness so input/screenshot tools can report "window
|
|
@@ -22,6 +23,7 @@ RuntimeLogBuffer.install();
|
|
|
22
23
|
// edit DM (write the flag) and the play-server DM (read+act on the flag) can
|
|
23
24
|
// access plugin:SetSetting/GetSetting.
|
|
24
25
|
StopPlayMonitor.init(plugin);
|
|
26
|
+
BreakpointHandlers.init(plugin);
|
|
25
27
|
ServerUrlSettings.init(plugin);
|
|
26
28
|
|
|
27
29
|
UI.init(plugin);
|