@chrrxs/robloxstudio-mcp 2.16.4 → 2.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +234 -151
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +1456 -416
- package/studio-plugin/MCPPlugin.rbxmx +1456 -416
- package/studio-plugin/src/modules/ClientBroker.ts +10 -0
- package/studio-plugin/src/modules/Communication.ts +11 -7
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +36 -10
- package/studio-plugin/src/modules/ServerUrlSettings.ts +62 -9
- package/studio-plugin/src/modules/UI.ts +11 -4
- package/studio-plugin/src/modules/Utils.ts +147 -13
- 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/ScriptHandlers.ts +3 -0
- package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +386 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +3 -209
- package/studio-plugin/src/server/index.server.ts +20 -5
|
@@ -5,6 +5,8 @@ import SceneAnalysisHandlers from "./handlers/SceneAnalysisHandlers";
|
|
|
5
5
|
import CaptureHandlers from "./handlers/CaptureHandlers";
|
|
6
6
|
import InputHandlers from "./handlers/InputHandlers";
|
|
7
7
|
import EvalRuntimeHandlers from "./handlers/EvalRuntimeHandlers";
|
|
8
|
+
import BreakpointHandlers from "./handlers/BreakpointHandlers";
|
|
9
|
+
import ScriptProfilerHandlers from "./handlers/ScriptProfilerHandlers";
|
|
8
10
|
import LuauExec from "./LuauExec";
|
|
9
11
|
import State from "./State";
|
|
10
12
|
import HttpDiagnostics from "./HttpDiagnostics";
|
|
@@ -94,6 +96,8 @@ const CLIENT_BROKER_ALLOWED_ENDPOINTS = new Set<string>([
|
|
|
94
96
|
"/api/get-runtime-logs",
|
|
95
97
|
"/api/get-memory-breakdown",
|
|
96
98
|
"/api/get-scene-analysis",
|
|
99
|
+
"/api/breakpoints",
|
|
100
|
+
"/api/capture-script-profiler",
|
|
97
101
|
"/api/multiplayer-test-state",
|
|
98
102
|
"/api/multiplayer-test-leave-client",
|
|
99
103
|
// Screenshot capture must run in the client peer (CaptureService captures
|
|
@@ -266,6 +270,12 @@ function setupClientBroker() {
|
|
|
266
270
|
if (payload && payload.endpoint === "/api/get-scene-analysis") {
|
|
267
271
|
return SceneAnalysisHandlers.getSceneAnalysis(payload.data ?? {});
|
|
268
272
|
}
|
|
273
|
+
if (payload && payload.endpoint === "/api/breakpoints") {
|
|
274
|
+
return BreakpointHandlers.breakpoints(payload.data ?? {});
|
|
275
|
+
}
|
|
276
|
+
if (payload && payload.endpoint === "/api/capture-script-profiler") {
|
|
277
|
+
return ScriptProfilerHandlers.captureScriptProfiler(payload.data ?? {});
|
|
278
|
+
}
|
|
269
279
|
if (payload && payload.endpoint === "/api/multiplayer-test-state") {
|
|
270
280
|
return handleMultiplayerTestState();
|
|
271
281
|
}
|
|
@@ -17,6 +17,8 @@ import LogHandlers from "./handlers/LogHandlers";
|
|
|
17
17
|
import SerializationHandlers from "./handlers/SerializationHandlers";
|
|
18
18
|
import MemoryHandlers from "./handlers/MemoryHandlers";
|
|
19
19
|
import SceneAnalysisHandlers from "./handlers/SceneAnalysisHandlers";
|
|
20
|
+
import BreakpointHandlers from "./handlers/BreakpointHandlers";
|
|
21
|
+
import ScriptProfilerHandlers from "./handlers/ScriptProfilerHandlers";
|
|
20
22
|
import EvalRuntimeHandlers from "./handlers/EvalRuntimeHandlers";
|
|
21
23
|
import ClientBroker from "./ClientBroker";
|
|
22
24
|
import ServerUrlSettings from "./ServerUrlSettings";
|
|
@@ -109,7 +111,6 @@ const routeMap: Record<string, Handler> = {
|
|
|
109
111
|
"/api/grep-scripts": QueryHandlers.grepScripts,
|
|
110
112
|
"/api/get-descendants": QueryHandlers.getDescendants,
|
|
111
113
|
"/api/compare-instances": QueryHandlers.compareInstances,
|
|
112
|
-
"/api/get-output-log": QueryHandlers.getOutputLog,
|
|
113
114
|
|
|
114
115
|
"/api/set-property": PropertyHandlers.setProperty,
|
|
115
116
|
"/api/set-properties": PropertyHandlers.setProperties,
|
|
@@ -146,13 +147,11 @@ const routeMap: Record<string, Handler> = {
|
|
|
146
147
|
|
|
147
148
|
"/api/start-playtest": TestHandlers.startPlaytest,
|
|
148
149
|
"/api/stop-playtest": TestHandlers.stopPlaytest,
|
|
149
|
-
"/api/get-playtest-output": TestHandlers.getPlaytestOutput,
|
|
150
150
|
"/api/multiplayer-test-start": TestHandlers.multiplayerTestStart,
|
|
151
151
|
"/api/multiplayer-test-state": TestHandlers.multiplayerTestState,
|
|
152
152
|
"/api/multiplayer-test-add-players": TestHandlers.multiplayerTestAddPlayers,
|
|
153
153
|
"/api/multiplayer-test-leave-client": TestHandlers.multiplayerTestLeaveClient,
|
|
154
154
|
"/api/multiplayer-test-end": TestHandlers.multiplayerTestEnd,
|
|
155
|
-
"/api/character-navigation": TestHandlers.characterNavigation,
|
|
156
155
|
|
|
157
156
|
"/api/export-build": BuildHandlers.exportBuild,
|
|
158
157
|
"/api/import-build": BuildHandlers.importBuild,
|
|
@@ -171,6 +170,8 @@ const routeMap: Record<string, Handler> = {
|
|
|
171
170
|
"/api/find-and-replace-in-scripts": ScriptHandlers.findAndReplaceInScripts,
|
|
172
171
|
|
|
173
172
|
"/api/get-runtime-logs": LogHandlers.getRuntimeLogs,
|
|
173
|
+
"/api/breakpoints": BreakpointHandlers.breakpoints,
|
|
174
|
+
"/api/capture-script-profiler": ScriptProfilerHandlers.captureScriptProfiler,
|
|
174
175
|
|
|
175
176
|
"/api/export-rbxm": SerializationHandlers.exportRbxm,
|
|
176
177
|
"/api/import-rbxm": SerializationHandlers.importRbxm,
|
|
@@ -306,6 +307,7 @@ function sendReady(conn: Connection): void {
|
|
|
306
307
|
lastReadyInstanceId = parseOk && typeIs(readyData.instanceId, "string") && readyData.instanceId !== ""
|
|
307
308
|
? readyData.instanceId
|
|
308
309
|
: instanceId;
|
|
310
|
+
ServerUrlSettings.rememberServerUrl(conn.serverUrl);
|
|
309
311
|
const connectedRole = assignedRole ?? detectRole();
|
|
310
312
|
if (readyFailureLogKeys.has(readyLogKey)) {
|
|
311
313
|
readyFailureLogKeys.delete(readyLogKey);
|
|
@@ -501,13 +503,15 @@ function activatePlugin(connIndex?: number) {
|
|
|
501
503
|
conn.currentRetryDelay = 0.5;
|
|
502
504
|
|
|
503
505
|
if (idx === State.getActiveTabIndex()) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if (
|
|
506
|
+
const normalizedUrl = ServerUrlSettings.normalizeServerUrl(ui.urlInput.Text);
|
|
507
|
+
conn.serverUrl = normalizedUrl !== "" ? normalizedUrl : conn.serverUrl;
|
|
508
|
+
if (conn.serverUrl === "") conn.serverUrl = ClientBroker.DEFAULT_MCP_URL;
|
|
509
|
+
ui.urlInput.Text = conn.serverUrl;
|
|
510
|
+
const port = ServerUrlSettings.extractPort(conn.serverUrl);
|
|
511
|
+
if (port !== undefined) conn.port = port;
|
|
507
512
|
UI.updateTabLabel(idx);
|
|
508
513
|
UI.updateUIState();
|
|
509
514
|
}
|
|
510
|
-
ServerUrlSettings.rememberServerUrl(conn.serverUrl);
|
|
511
515
|
UI.updateTabDot(idx);
|
|
512
516
|
|
|
513
517
|
if (!conn.heartbeatConnection) {
|
|
@@ -57,21 +57,47 @@ function dropOldestUntilFits(incomingBytes: number): void {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function pushEntry(msg: string, t: Enum.MessageType, ts = nowSec()): void {
|
|
61
|
+
const bytes = msg.size();
|
|
62
|
+
dropOldestUntilFits(bytes);
|
|
63
|
+
entries.push({
|
|
64
|
+
seq: nextSeq,
|
|
65
|
+
ts,
|
|
66
|
+
level: levelTag(t),
|
|
67
|
+
message: msg,
|
|
68
|
+
});
|
|
69
|
+
nextSeq += 1;
|
|
70
|
+
totalBytes += bytes;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface LogHistoryEntry {
|
|
74
|
+
message: string;
|
|
75
|
+
messageType: Enum.MessageType;
|
|
76
|
+
timestamp: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function seedRuntimeHistory(): void {
|
|
80
|
+
if (!RunService.IsRunning()) return;
|
|
81
|
+
|
|
82
|
+
const [ok, history] = pcall(() => LogService.GetLogHistory() as LogHistoryEntry[]);
|
|
83
|
+
if (!ok) return;
|
|
84
|
+
|
|
85
|
+
for (const entry of history) {
|
|
86
|
+
if (!typeIs(entry.message, "string")) continue;
|
|
87
|
+
pushEntry(entry.message, entry.messageType, typeIs(entry.timestamp, "number") ? entry.timestamp : undefined);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
60
91
|
function install(): void {
|
|
61
92
|
if (installed) return;
|
|
62
93
|
if (!RunService.IsStudio()) return;
|
|
63
94
|
installed = true;
|
|
95
|
+
// Play peers can emit startup logs before the plugin finishes loading.
|
|
96
|
+
// Seed from per-DataModel LogHistory so get_runtime_logs can still see
|
|
97
|
+
// those early messages; skip edit mode to avoid stale prior-session logs.
|
|
98
|
+
seedRuntimeHistory();
|
|
64
99
|
LogService.MessageOut.Connect((msg, t) => {
|
|
65
|
-
|
|
66
|
-
dropOldestUntilFits(bytes);
|
|
67
|
-
entries.push({
|
|
68
|
-
seq: nextSeq,
|
|
69
|
-
ts: nowSec(),
|
|
70
|
-
level: levelTag(t),
|
|
71
|
-
message: msg,
|
|
72
|
-
});
|
|
73
|
-
nextSeq += 1;
|
|
74
|
-
totalBytes += bytes;
|
|
100
|
+
pushEntry(msg, t);
|
|
75
101
|
});
|
|
76
102
|
}
|
|
77
103
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { HttpService, ServerStorage } from "@rbxts/services";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const LEGACY_SETTING_KEY_PREFIX = "MCP_SERVER_URL_";
|
|
4
|
+
const SETTING_KEY_PREFIX = "MCP_LAST_SUCCESSFUL_SERVER_URL_";
|
|
5
|
+
const GLOBAL_SETTING_KEY = "MCP_LAST_SUCCESSFUL_SERVER_URL_GLOBAL_V1";
|
|
4
6
|
|
|
5
7
|
let pluginRef: Plugin | undefined;
|
|
6
8
|
|
|
@@ -8,6 +10,31 @@ function init(p: Plugin): void {
|
|
|
8
10
|
pluginRef = p;
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
function normalizeServerUrl(serverUrl: string | undefined): string {
|
|
14
|
+
let normalized = (serverUrl ?? "").gsub("^%s+", "")[0].gsub("%s+$", "")[0];
|
|
15
|
+
if (normalized === "") return "";
|
|
16
|
+
|
|
17
|
+
if (normalized.match("^%a[%w+.-]*://")[0] === undefined) {
|
|
18
|
+
normalized = `http://${normalized}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
while (
|
|
22
|
+
normalized.size() > 0 &&
|
|
23
|
+
normalized.sub(-1) === "/" &&
|
|
24
|
+
normalized.match("^%a[%w+.-]*://$")[0] === undefined
|
|
25
|
+
) {
|
|
26
|
+
normalized = normalized.sub(1, -2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractPort(serverUrl: string): number | undefined {
|
|
33
|
+
const [portStr] = serverUrl.match(":(%d+)$");
|
|
34
|
+
if (portStr === undefined) return undefined;
|
|
35
|
+
return tonumber(portStr);
|
|
36
|
+
}
|
|
37
|
+
|
|
11
38
|
function addUnique(values: string[], value: string): void {
|
|
12
39
|
if (!values.includes(value)) {
|
|
13
40
|
values.push(value);
|
|
@@ -34,28 +61,54 @@ function settingKey(instanceId: string): string {
|
|
|
34
61
|
return SETTING_KEY_PREFIX + instanceId;
|
|
35
62
|
}
|
|
36
63
|
|
|
64
|
+
function legacySettingKey(instanceId: string): string {
|
|
65
|
+
return LEGACY_SETTING_KEY_PREFIX + instanceId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readSettingString(key: string): string | undefined {
|
|
69
|
+
if (!pluginRef) return undefined;
|
|
70
|
+
const [ok, value] = pcall(() => pluginRef!.GetSetting(key));
|
|
71
|
+
if (!ok || !typeIs(value, "string")) return undefined;
|
|
72
|
+
|
|
73
|
+
const normalized = normalizeServerUrl(value as string);
|
|
74
|
+
return normalized !== "" ? normalized : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeSettingString(key: string, serverUrl: string): void {
|
|
78
|
+
if (!pluginRef) return;
|
|
79
|
+
pcall(() => pluginRef!.SetSetting(key, serverUrl));
|
|
80
|
+
}
|
|
81
|
+
|
|
37
82
|
function rememberServerUrl(serverUrl: string): void {
|
|
38
|
-
|
|
83
|
+
const normalized = normalizeServerUrl(serverUrl);
|
|
84
|
+
if (!pluginRef || normalized === "") return;
|
|
85
|
+
writeSettingString(GLOBAL_SETTING_KEY, normalized);
|
|
39
86
|
for (const instanceId of computeInstanceIds()) {
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
writeSettingString(settingKey(instanceId), normalized);
|
|
88
|
+
writeSettingString(legacySettingKey(instanceId), normalized);
|
|
42
89
|
}
|
|
43
90
|
}
|
|
44
91
|
|
|
45
92
|
function readServerUrl(): string | undefined {
|
|
46
93
|
if (!pluginRef) return undefined;
|
|
47
94
|
for (const instanceId of computeInstanceIds()) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
if (ok && typeIs(value, "string") && value !== "") {
|
|
51
|
-
return value as string;
|
|
52
|
-
}
|
|
95
|
+
const remembered = readSettingString(settingKey(instanceId));
|
|
96
|
+
if (remembered !== undefined) return remembered;
|
|
53
97
|
}
|
|
98
|
+
const globalRemembered = readSettingString(GLOBAL_SETTING_KEY);
|
|
99
|
+
if (globalRemembered !== undefined) return globalRemembered;
|
|
100
|
+
for (const instanceId of computeInstanceIds()) {
|
|
101
|
+
const legacyRemembered = readSettingString(legacySettingKey(instanceId));
|
|
102
|
+
if (legacyRemembered !== undefined) return legacyRemembered;
|
|
103
|
+
}
|
|
104
|
+
|
|
54
105
|
return undefined;
|
|
55
106
|
}
|
|
56
107
|
|
|
57
108
|
export = {
|
|
58
109
|
init,
|
|
110
|
+
normalizeServerUrl,
|
|
111
|
+
extractPort,
|
|
59
112
|
rememberServerUrl,
|
|
60
113
|
readServerUrl,
|
|
61
114
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TweenService } from "@rbxts/services";
|
|
2
2
|
import State from "./State";
|
|
3
|
+
import ServerUrlSettings from "./ServerUrlSettings";
|
|
3
4
|
import { Connection } from "../types";
|
|
4
5
|
|
|
5
6
|
interface UIElements {
|
|
@@ -473,7 +474,7 @@ function init(pluginRef: Plugin) {
|
|
|
473
474
|
urlInput.Size = new UDim2(1, 0, 0, 26);
|
|
474
475
|
urlInput.BackgroundColor3 = C.bg;
|
|
475
476
|
urlInput.BorderSizePixel = 0;
|
|
476
|
-
urlInput.Text =
|
|
477
|
+
urlInput.Text = State.getActiveConnection().serverUrl;
|
|
477
478
|
urlInput.TextColor3 = C.label;
|
|
478
479
|
urlInput.TextSize = 11;
|
|
479
480
|
urlInput.Font = Enum.Font.GothamMedium;
|
|
@@ -495,9 +496,15 @@ function init(pluginRef: Plugin) {
|
|
|
495
496
|
urlInput.FocusLost.Connect(() => {
|
|
496
497
|
const conn = State.getActiveConnection();
|
|
497
498
|
if (!conn || conn.isActive) return;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
499
|
+
const normalizedUrl = ServerUrlSettings.normalizeServerUrl(urlInput.Text);
|
|
500
|
+
if (normalizedUrl === "") {
|
|
501
|
+
urlInput.Text = conn.serverUrl;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
conn.serverUrl = normalizedUrl;
|
|
505
|
+
urlInput.Text = normalizedUrl;
|
|
506
|
+
const port = ServerUrlSettings.extractPort(conn.serverUrl);
|
|
507
|
+
if (port !== undefined) conn.port = port;
|
|
501
508
|
updateTabLabel(State.getActiveTabIndex());
|
|
502
509
|
});
|
|
503
510
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const ScriptEditorService = game.GetService("ScriptEditorService");
|
|
2
2
|
|
|
3
|
+
const LUAU_KEYWORDS = new Set<string>([
|
|
4
|
+
"and", "break", "continue", "do", "else", "elseif", "end", "export",
|
|
5
|
+
"false", "for", "function", "if", "in", "local", "nil", "not", "or",
|
|
6
|
+
"repeat", "return", "then", "true", "type", "until", "while",
|
|
7
|
+
]);
|
|
8
|
+
|
|
3
9
|
function safeCall<T>(func: (...args: never[]) => T, ...args: never[]): T | undefined {
|
|
4
10
|
const [success, result] = pcall(func, ...args);
|
|
5
11
|
if (success) {
|
|
@@ -10,6 +16,125 @@ function safeCall<T>(func: (...args: never[]) => T, ...args: never[]): T | undef
|
|
|
10
16
|
}
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
function isSimplePathSegment(segment: string): boolean {
|
|
20
|
+
return segment.match("^[%a_][%w_]*$")[0] !== undefined && !LUAU_KEYWORDS.has(segment);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function quotePathSegment(segment: string): string {
|
|
24
|
+
let escaped = segment.gsub("\\", "\\\\")[0];
|
|
25
|
+
escaped = escaped.gsub("\n", "\\n")[0];
|
|
26
|
+
escaped = escaped.gsub("\r", "\\r")[0];
|
|
27
|
+
escaped = escaped.gsub("\t", "\\t")[0];
|
|
28
|
+
escaped = escaped.gsub('"', '\\"')[0];
|
|
29
|
+
return `"${escaped}"`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function unescapePathSegment(segment: string): string {
|
|
33
|
+
const chars: string[] = [];
|
|
34
|
+
let i = 1;
|
|
35
|
+
while (i <= segment.size()) {
|
|
36
|
+
const ch = segment.sub(i, i);
|
|
37
|
+
if (ch === "\\" && i < segment.size()) {
|
|
38
|
+
const nextChar = segment.sub(i + 1, i + 1);
|
|
39
|
+
if (nextChar === "n") {
|
|
40
|
+
chars.push("\n");
|
|
41
|
+
} else if (nextChar === "r") {
|
|
42
|
+
chars.push("\r");
|
|
43
|
+
} else if (nextChar === "t") {
|
|
44
|
+
chars.push("\t");
|
|
45
|
+
} else {
|
|
46
|
+
chars.push(nextChar);
|
|
47
|
+
}
|
|
48
|
+
i += 2;
|
|
49
|
+
} else {
|
|
50
|
+
chars.push(ch);
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return chars.join("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isCanonicalBracketStart(path: string, index: number): boolean {
|
|
58
|
+
const quote = path.sub(index + 1, index + 1);
|
|
59
|
+
return (quote === '"' || quote === "'") && path.sub(index - 1, index - 1) !== ".";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseInstancePath(path: string): string[] | undefined {
|
|
63
|
+
let i = 1;
|
|
64
|
+
const len = path.size();
|
|
65
|
+
const parts: string[] = [];
|
|
66
|
+
let current = "";
|
|
67
|
+
|
|
68
|
+
if (path === "" || path === "game") return parts;
|
|
69
|
+
if (path.sub(1, 5) === "game.") {
|
|
70
|
+
i = 6;
|
|
71
|
+
} else if (path.sub(1, 5) === "game[") {
|
|
72
|
+
i = 5;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
while (i <= len) {
|
|
76
|
+
const ch = path.sub(i, i);
|
|
77
|
+
|
|
78
|
+
if (ch === ".") {
|
|
79
|
+
if (current !== "") {
|
|
80
|
+
parts.push(current);
|
|
81
|
+
current = "";
|
|
82
|
+
i += 1;
|
|
83
|
+
} else if (i > 1 && path.sub(i - 1, i - 1) === "." && i < len && path.sub(i + 1, i + 1) !== "[") {
|
|
84
|
+
// Back-compat for previously emitted paths such as
|
|
85
|
+
// game.ServerScriptService..dir.ReproScript, where ".dir"
|
|
86
|
+
// was an actual instance name.
|
|
87
|
+
current = ".";
|
|
88
|
+
i += 1;
|
|
89
|
+
} else {
|
|
90
|
+
i += 1;
|
|
91
|
+
}
|
|
92
|
+
} else if (ch === "[" && i < len && isCanonicalBracketStart(path, i)) {
|
|
93
|
+
if (current !== "") {
|
|
94
|
+
parts.push(current);
|
|
95
|
+
current = "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const quote = path.sub(i + 1, i + 1);
|
|
99
|
+
if (quote !== '"' && quote !== "'") return undefined;
|
|
100
|
+
let j = i + 2;
|
|
101
|
+
let raw = "";
|
|
102
|
+
while (j <= len) {
|
|
103
|
+
const c = path.sub(j, j);
|
|
104
|
+
if (c === "\\") {
|
|
105
|
+
if (j >= len) return undefined;
|
|
106
|
+
raw += c + path.sub(j + 1, j + 1);
|
|
107
|
+
j += 2;
|
|
108
|
+
} else if (c === quote) {
|
|
109
|
+
break;
|
|
110
|
+
} else {
|
|
111
|
+
raw += c;
|
|
112
|
+
j += 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (j > len || path.sub(j, j) !== quote || path.sub(j + 1, j + 1) !== "]") return undefined;
|
|
116
|
+
parts.push(unescapePathSegment(raw));
|
|
117
|
+
i = j + 2;
|
|
118
|
+
} else {
|
|
119
|
+
current += ch;
|
|
120
|
+
i += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (current !== "") parts.push(current);
|
|
125
|
+
return parts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getRootSegment(instance: Instance): string {
|
|
129
|
+
if (instance.Parent === game) {
|
|
130
|
+
const [ok, service] = pcall(() => game.GetService(instance.ClassName as keyof Services));
|
|
131
|
+
if (ok && service === instance) {
|
|
132
|
+
return instance.ClassName;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return instance.Name;
|
|
136
|
+
}
|
|
137
|
+
|
|
13
138
|
function getInstancePath(instance: Instance): string {
|
|
14
139
|
if (!instance || instance === game) {
|
|
15
140
|
return "game";
|
|
@@ -19,26 +144,35 @@ function getInstancePath(instance: Instance): string {
|
|
|
19
144
|
let current: Instance | undefined = instance;
|
|
20
145
|
|
|
21
146
|
while (current && current !== game) {
|
|
22
|
-
pathParts.unshift(current
|
|
147
|
+
pathParts.unshift(getRootSegment(current));
|
|
23
148
|
current = current.Parent as Instance | undefined;
|
|
24
149
|
}
|
|
25
150
|
|
|
26
|
-
|
|
151
|
+
let path = "game";
|
|
152
|
+
for (const part of pathParts) {
|
|
153
|
+
if (isSimplePathSegment(part)) {
|
|
154
|
+
path += `.${part}`;
|
|
155
|
+
} else {
|
|
156
|
+
path += `[${quotePathSegment(part)}]`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return path;
|
|
27
160
|
}
|
|
28
161
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
162
|
+
function getRootInstance(segment: string): Instance | undefined {
|
|
163
|
+
const [ok, service] = pcall(() => game.GetService(segment as keyof Services));
|
|
164
|
+
if (ok && service) return service as Instance;
|
|
165
|
+
return game.FindFirstChild(segment);
|
|
166
|
+
}
|
|
33
167
|
|
|
34
|
-
|
|
35
|
-
const parts
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
168
|
+
function getInstanceByPath(path: string): Instance | undefined {
|
|
169
|
+
const parts = parseInstancePath(path);
|
|
170
|
+
if (parts === undefined) return undefined;
|
|
171
|
+
if (parts.size() === 0) return game;
|
|
39
172
|
|
|
40
|
-
let current: Instance | undefined =
|
|
41
|
-
for (
|
|
173
|
+
let current: Instance | undefined = getRootInstance(parts[0]);
|
|
174
|
+
for (let i = 1; i < parts.size(); i++) {
|
|
175
|
+
const part = parts[i];
|
|
42
176
|
if (!current) return undefined;
|
|
43
177
|
current = current.FindFirstChild(part);
|
|
44
178
|
}
|