@princeofscale/bloxforge-inspector 2.20.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/assets/Baseplate.rbxl +0 -0
- package/dist/index.js +19281 -0
- package/package.json +62 -0
- package/studio-plugin/INSTALLATION.md +161 -0
- package/studio-plugin/MCPInspectorPlugin.rbxmx +171699 -0
- package/studio-plugin/MCPPlugin.rbxmx +171699 -0
- package/studio-plugin/default.project.json +19 -0
- package/studio-plugin/dev.project.json +23 -0
- package/studio-plugin/include/LibMP.lua +156378 -0
- package/studio-plugin/inspector-icon.png +0 -0
- package/studio-plugin/package-lock.json +692 -0
- package/studio-plugin/package.json +19 -0
- package/studio-plugin/plugin.json +10 -0
- package/studio-plugin/src/modules/ClientBroker.ts +447 -0
- package/studio-plugin/src/modules/Communication.ts +697 -0
- package/studio-plugin/src/modules/EvalBridges.ts +255 -0
- package/studio-plugin/src/modules/HttpDiagnostics.ts +50 -0
- package/studio-plugin/src/modules/JobRegistry.ts +77 -0
- package/studio-plugin/src/modules/LuauExec.ts +465 -0
- package/studio-plugin/src/modules/Recording.ts +28 -0
- package/studio-plugin/src/modules/RenderMonitor.ts +60 -0
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +152 -0
- package/studio-plugin/src/modules/ServerUrlSettings.ts +114 -0
- package/studio-plugin/src/modules/State.ts +101 -0
- package/studio-plugin/src/modules/StopPlayMonitor.ts +276 -0
- package/studio-plugin/src/modules/UI.ts +790 -0
- package/studio-plugin/src/modules/Utils.ts +318 -0
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +241 -0
- package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +460 -0
- package/studio-plugin/src/modules/handlers/BuildHandlers.ts +481 -0
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +203 -0
- package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +149 -0
- package/studio-plugin/src/modules/handlers/InputHandlers.ts +160 -0
- package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +380 -0
- package/studio-plugin/src/modules/handlers/JobHandlers.ts +111 -0
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +14 -0
- package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +44 -0
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +354 -0
- package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +1263 -0
- package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +191 -0
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +809 -0
- package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +216 -0
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +530 -0
- package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +386 -0
- package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +172 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +535 -0
- package/studio-plugin/src/server/index.server.ts +134 -0
- package/studio-plugin/src/types/index.d.ts +83 -0
- package/studio-plugin/tsconfig.json +21 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Async execute_luau handlers. execute_luau_async registers a job, spawns the
|
|
2
|
+
// real LuauExec.execute in a coroutine, and returns the jobId immediately;
|
|
3
|
+
// get_job_status / get_job_result poll the registry. This keeps every MCP call
|
|
4
|
+
// fast (no long-poll timeout) while heavy Luau runs between polls. Cancellation
|
|
5
|
+
// is best-effort — Luau coroutines can't be force-killed, so we flag the job and
|
|
6
|
+
// discard its result on completion.
|
|
7
|
+
|
|
8
|
+
import LuauExec from "../LuauExec";
|
|
9
|
+
import JobRegistry from "../JobRegistry";
|
|
10
|
+
|
|
11
|
+
// Install a sanctioned progress/cancel API once. Server-generated long-running
|
|
12
|
+
// Luau (terrain/template/batch builders) can call _G.__mcp.progress(done,total,msg)
|
|
13
|
+
// and _G.__mcp.checkCancelled() to report progress and bail early. We do NOT
|
|
14
|
+
// auto-inject this into arbitrary user code — it's an opt-in cooperative API.
|
|
15
|
+
declare const _G: { __mcp?: unknown };
|
|
16
|
+
function installMcpGlobal(): void {
|
|
17
|
+
if (_G.__mcp !== undefined) return;
|
|
18
|
+
_G.__mcp = {
|
|
19
|
+
progress: (done: number, total?: number, message?: string, stage?: string) => {
|
|
20
|
+
JobRegistry.reportProgress(coroutine.running(), done, total, message, stage);
|
|
21
|
+
},
|
|
22
|
+
checkCancelled: () => JobRegistry.isCancelledForThread(coroutine.running()),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
installMcpGlobal();
|
|
26
|
+
|
|
27
|
+
function executeLuauAsync(requestData: Record<string, unknown>) {
|
|
28
|
+
const code = requestData.code as string;
|
|
29
|
+
if (!code || code === "") return { error: "Code is required" };
|
|
30
|
+
|
|
31
|
+
const job = JobRegistry.create();
|
|
32
|
+
task.spawn(() => {
|
|
33
|
+
const co = coroutine.running();
|
|
34
|
+
JobRegistry.bindThread(co, job.id);
|
|
35
|
+
const result = LuauExec.execute(code);
|
|
36
|
+
JobRegistry.unbindThread(co);
|
|
37
|
+
const j = JobRegistry.get(job.id);
|
|
38
|
+
if (!j) return;
|
|
39
|
+
if (j.cancelled === true) {
|
|
40
|
+
j.status = "cancelled";
|
|
41
|
+
j.finishedAt = tick();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
j.success = result.success;
|
|
45
|
+
j.returnValue = result.returnValue;
|
|
46
|
+
j.output = result.output;
|
|
47
|
+
j.error = result.error;
|
|
48
|
+
j.message = result.message;
|
|
49
|
+
j.status = result.success ? "done" : "error";
|
|
50
|
+
j.finishedAt = tick();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return { jobId: job.id, status: "running", estimatedKind: "long" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getJobStatus(requestData: Record<string, unknown>) {
|
|
57
|
+
const jobId = requestData.jobId as string;
|
|
58
|
+
if (!jobId || jobId === "") return { error: "jobId is required" };
|
|
59
|
+
const j = JobRegistry.get(jobId);
|
|
60
|
+
if (!j) return { error: "Unknown jobId", jobId };
|
|
61
|
+
const elapsed = (j.finishedAt ?? tick()) - j.startedAt;
|
|
62
|
+
return {
|
|
63
|
+
jobId: j.id,
|
|
64
|
+
status: j.status,
|
|
65
|
+
elapsed,
|
|
66
|
+
message: j.message,
|
|
67
|
+
progress: j.progress,
|
|
68
|
+
total: j.total,
|
|
69
|
+
stage: j.stage,
|
|
70
|
+
done: j.status !== "running",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getJobResult(requestData: Record<string, unknown>) {
|
|
75
|
+
const jobId = requestData.jobId as string;
|
|
76
|
+
if (!jobId || jobId === "") return { error: "jobId is required" };
|
|
77
|
+
const j = JobRegistry.get(jobId);
|
|
78
|
+
if (!j) return { error: "Unknown jobId", jobId };
|
|
79
|
+
if (j.status === "running") {
|
|
80
|
+
return { jobId: j.id, status: "running", done: false };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
jobId: j.id,
|
|
84
|
+
status: j.status,
|
|
85
|
+
done: true,
|
|
86
|
+
success: j.success,
|
|
87
|
+
returnValue: j.returnValue,
|
|
88
|
+
output: j.output,
|
|
89
|
+
error: j.error,
|
|
90
|
+
message: j.message,
|
|
91
|
+
elapsed: (j.finishedAt ?? tick()) - j.startedAt,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cancelJob(requestData: Record<string, unknown>) {
|
|
96
|
+
const jobId = requestData.jobId as string;
|
|
97
|
+
if (!jobId || jobId === "") return { error: "jobId is required" };
|
|
98
|
+
const j = JobRegistry.get(jobId);
|
|
99
|
+
if (!j) return { error: "Unknown jobId", jobId };
|
|
100
|
+
if (j.status === "running") {
|
|
101
|
+
j.cancelled = true;
|
|
102
|
+
return {
|
|
103
|
+
jobId: j.id,
|
|
104
|
+
status: "cancelling",
|
|
105
|
+
note: "Luau coroutines cannot be force-killed; the result will be discarded on completion.",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { jobId: j.id, status: j.status, note: "Job already finished." };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export = { executeLuauAsync, getJobStatus, getJobResult, cancelJob };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import RuntimeLogBuffer from "../RuntimeLogBuffer";
|
|
2
|
+
|
|
3
|
+
function getRuntimeLogs(requestData: Record<string, unknown>): unknown {
|
|
4
|
+
const since = requestData.since as number | undefined;
|
|
5
|
+
const tail = requestData.tail as number | undefined;
|
|
6
|
+
const filter = requestData.filter as string | undefined;
|
|
7
|
+
// This is the buffer that captured the LogService event, not necessarily
|
|
8
|
+
// the script-origin peer. Ordinary playtests share/reflect logs across
|
|
9
|
+
// edit/server/client LogService buffers.
|
|
10
|
+
const capturedBy = RuntimeLogBuffer.detectPeer();
|
|
11
|
+
return RuntimeLogBuffer.query({ since, tail, filter }, capturedBy);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export = { getRuntimeLogs };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const Stats = game.GetService("Stats");
|
|
2
|
+
|
|
3
|
+
// GetMemoryUsageMbAllCategories is gated by capability "InternalTest" and not
|
|
4
|
+
// callable from plugin context. GetMemoryUsageMbForTag is not - so we iterate
|
|
5
|
+
// Enum.DeveloperMemoryTag and ask per-tag.
|
|
6
|
+
function getMemoryBreakdown(requestData: Record<string, unknown>): unknown {
|
|
7
|
+
if (!Stats.MemoryTrackingEnabled) {
|
|
8
|
+
return { error: "MemoryTrackingEnabled is false on this peer" };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const requested = requestData.tags as string[] | undefined;
|
|
12
|
+
const requestedSet = requested && requested.size() > 0 ? new Set(requested) : undefined;
|
|
13
|
+
|
|
14
|
+
const categories: Record<string, number> = {};
|
|
15
|
+
for (const item of Enum.DeveloperMemoryTag.GetEnumItems()) {
|
|
16
|
+
const name = item.Name;
|
|
17
|
+
if (requestedSet && !requestedSet.has(name)) continue;
|
|
18
|
+
const [ok, mb] = pcall(() => Stats.GetMemoryUsageMbForTag(item));
|
|
19
|
+
categories[name] = ok ? (mb as number) : 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const unknownTags: string[] = [];
|
|
23
|
+
if (requestedSet) {
|
|
24
|
+
const known = new Set<string>();
|
|
25
|
+
for (const i of Enum.DeveloperMemoryTag.GetEnumItems()) known.add(i.Name);
|
|
26
|
+
for (const t of requestedSet) {
|
|
27
|
+
if (!known.has(t)) {
|
|
28
|
+
unknownTags.push(t);
|
|
29
|
+
categories[t] = 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result: Record<string, unknown> = {
|
|
35
|
+
total_mb: Stats.GetTotalMemoryUsageMb(),
|
|
36
|
+
categories,
|
|
37
|
+
memory_tracking_enabled: true,
|
|
38
|
+
timestamp: DateTime.now().UnixTimestampMillis,
|
|
39
|
+
};
|
|
40
|
+
if (unknownTags.size() > 0) result.unknown_tags = unknownTags;
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export = { getMemoryBreakdown };
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { CollectionService } from "@rbxts/services";
|
|
2
|
+
import Utils from "../Utils";
|
|
3
|
+
import Recording from "../Recording";
|
|
4
|
+
import LuauExec from "../LuauExec";
|
|
5
|
+
|
|
6
|
+
const ChangeHistoryService = game.GetService("ChangeHistoryService");
|
|
7
|
+
const Selection = game.GetService("Selection");
|
|
8
|
+
|
|
9
|
+
const { getInstancePath, getInstanceByPath } = Utils;
|
|
10
|
+
const { beginRecording, finishRecording } = Recording;
|
|
11
|
+
|
|
12
|
+
function serializeValue(value: unknown): unknown {
|
|
13
|
+
const vType = typeOf(value);
|
|
14
|
+
if (vType === "Vector3") {
|
|
15
|
+
const v = value as Vector3;
|
|
16
|
+
return { X: v.X, Y: v.Y, Z: v.Z, _type: "Vector3" };
|
|
17
|
+
} else if (vType === "Color3") {
|
|
18
|
+
const v = value as Color3;
|
|
19
|
+
return { R: v.R, G: v.G, B: v.B, _type: "Color3" };
|
|
20
|
+
} else if (vType === "CFrame") {
|
|
21
|
+
const v = value as CFrame;
|
|
22
|
+
return { Position: { X: v.Position.X, Y: v.Position.Y, Z: v.Position.Z }, _type: "CFrame" };
|
|
23
|
+
} else if (vType === "UDim2") {
|
|
24
|
+
const v = value as UDim2;
|
|
25
|
+
return {
|
|
26
|
+
X: { Scale: v.X.Scale, Offset: v.X.Offset },
|
|
27
|
+
Y: { Scale: v.Y.Scale, Offset: v.Y.Offset },
|
|
28
|
+
_type: "UDim2",
|
|
29
|
+
};
|
|
30
|
+
} else if (vType === "BrickColor") {
|
|
31
|
+
const v = value as BrickColor;
|
|
32
|
+
return { Name: v.Name, _type: "BrickColor" };
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function deserializeValue(attributeValue: unknown, valueType?: string): unknown {
|
|
38
|
+
if (!typeIs(attributeValue, "table")) return attributeValue;
|
|
39
|
+
|
|
40
|
+
const tbl = attributeValue as Record<string, unknown>;
|
|
41
|
+
const t = (tbl._type as string) ?? valueType;
|
|
42
|
+
|
|
43
|
+
if (t === "Vector3") {
|
|
44
|
+
return new Vector3((tbl.X as number) ?? 0, (tbl.Y as number) ?? 0, (tbl.Z as number) ?? 0);
|
|
45
|
+
} else if (t === "Color3") {
|
|
46
|
+
return new Color3((tbl.R as number) ?? 0, (tbl.G as number) ?? 0, (tbl.B as number) ?? 0);
|
|
47
|
+
} else if (t === "UDim2") {
|
|
48
|
+
const x = tbl.X as Record<string, number> | undefined;
|
|
49
|
+
const y = tbl.Y as Record<string, number> | undefined;
|
|
50
|
+
return new UDim2(x?.Scale ?? 0, x?.Offset ?? 0, y?.Scale ?? 0, y?.Offset ?? 0);
|
|
51
|
+
} else if (t === "BrickColor") {
|
|
52
|
+
return new BrickColor(((tbl.Name as string) ?? "Medium stone grey") as unknown as number);
|
|
53
|
+
}
|
|
54
|
+
return attributeValue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setAttribute(requestData: Record<string, unknown>) {
|
|
58
|
+
const instancePath = requestData.instancePath as string;
|
|
59
|
+
const attributeName = requestData.attributeName as string;
|
|
60
|
+
const attributeValue = requestData.attributeValue;
|
|
61
|
+
const valueType = requestData.valueType as string | undefined;
|
|
62
|
+
|
|
63
|
+
if (!instancePath || !attributeName) {
|
|
64
|
+
return { error: "Instance path and attribute name are required" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const instance = getInstanceByPath(instancePath);
|
|
68
|
+
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
69
|
+
const recordingId = beginRecording(`Set attribute ${attributeName} on ${instance.Name}`);
|
|
70
|
+
|
|
71
|
+
const [success, result] = pcall(() => {
|
|
72
|
+
const value = deserializeValue(attributeValue, valueType);
|
|
73
|
+
instance.SetAttribute(attributeName, value as AttributeValue);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true, instancePath, attributeName,
|
|
77
|
+
value: attributeValue, message: "Attribute set successfully",
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (success) {
|
|
82
|
+
finishRecording(recordingId, true);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
finishRecording(recordingId, false);
|
|
86
|
+
return { error: `Failed to set attribute: ${result}` };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getAttributes(requestData: Record<string, unknown>) {
|
|
90
|
+
const instancePath = requestData.instancePath as string;
|
|
91
|
+
if (!instancePath) return { error: "Instance path is required" };
|
|
92
|
+
|
|
93
|
+
const instance = getInstanceByPath(instancePath);
|
|
94
|
+
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
95
|
+
|
|
96
|
+
const [success, result] = pcall(() => {
|
|
97
|
+
const attributes = instance.GetAttributes();
|
|
98
|
+
const serializedAttributes: Record<string, { value: unknown; type: string }> = {};
|
|
99
|
+
let count = 0;
|
|
100
|
+
|
|
101
|
+
for (const [name, value] of pairs(attributes)) {
|
|
102
|
+
serializedAttributes[name as string] = {
|
|
103
|
+
value: serializeValue(value),
|
|
104
|
+
type: typeOf(value),
|
|
105
|
+
};
|
|
106
|
+
count++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { instancePath, attributes: serializedAttributes, count };
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (success) return result;
|
|
113
|
+
return { error: `Failed to get attributes: ${result}` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function deleteAttribute(requestData: Record<string, unknown>) {
|
|
117
|
+
const instancePath = requestData.instancePath as string;
|
|
118
|
+
const attributeName = requestData.attributeName as string;
|
|
119
|
+
|
|
120
|
+
if (!instancePath || !attributeName) {
|
|
121
|
+
return { error: "Instance path and attribute name are required" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const instance = getInstanceByPath(instancePath);
|
|
125
|
+
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
126
|
+
const recordingId = beginRecording(`Delete attribute ${attributeName} from ${instance.Name}`);
|
|
127
|
+
|
|
128
|
+
const [success, result] = pcall(() => {
|
|
129
|
+
const existed = instance.GetAttribute(attributeName) !== undefined;
|
|
130
|
+
instance.SetAttribute(attributeName, undefined);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
success: true, instancePath, attributeName, existed,
|
|
134
|
+
message: existed ? "Attribute deleted successfully" : "Attribute did not exist",
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (success) {
|
|
139
|
+
finishRecording(recordingId, true);
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
finishRecording(recordingId, false);
|
|
143
|
+
return { error: `Failed to delete attribute: ${result}` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getTags(requestData: Record<string, unknown>) {
|
|
147
|
+
const instancePath = requestData.instancePath as string;
|
|
148
|
+
if (!instancePath) return { error: "Instance path is required" };
|
|
149
|
+
|
|
150
|
+
const instance = getInstanceByPath(instancePath);
|
|
151
|
+
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
152
|
+
|
|
153
|
+
const [success, result] = pcall(() => {
|
|
154
|
+
const tags = CollectionService.GetTags(instance);
|
|
155
|
+
return { instancePath, tags, count: tags.size() };
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (success) return result;
|
|
159
|
+
return { error: `Failed to get tags: ${result}` };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function addTag(requestData: Record<string, unknown>) {
|
|
163
|
+
const instancePath = requestData.instancePath as string;
|
|
164
|
+
const tagName = requestData.tagName as string;
|
|
165
|
+
|
|
166
|
+
if (!instancePath || !tagName) {
|
|
167
|
+
return { error: "Instance path and tag name are required" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const instance = getInstanceByPath(instancePath);
|
|
171
|
+
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
172
|
+
const recordingId = beginRecording(`Add tag ${tagName} to ${instance.Name}`);
|
|
173
|
+
|
|
174
|
+
const [success, result] = pcall(() => {
|
|
175
|
+
const alreadyHad = CollectionService.HasTag(instance, tagName);
|
|
176
|
+
CollectionService.AddTag(instance, tagName);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
success: true, instancePath, tagName, alreadyHad,
|
|
180
|
+
message: alreadyHad ? "Instance already had this tag" : "Tag added successfully",
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (success) {
|
|
185
|
+
finishRecording(recordingId, true);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
finishRecording(recordingId, false);
|
|
189
|
+
return { error: `Failed to add tag: ${result}` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function removeTag(requestData: Record<string, unknown>) {
|
|
193
|
+
const instancePath = requestData.instancePath as string;
|
|
194
|
+
const tagName = requestData.tagName as string;
|
|
195
|
+
|
|
196
|
+
if (!instancePath || !tagName) {
|
|
197
|
+
return { error: "Instance path and tag name are required" };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const instance = getInstanceByPath(instancePath);
|
|
201
|
+
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
202
|
+
const recordingId = beginRecording(`Remove tag ${tagName} from ${instance.Name}`);
|
|
203
|
+
|
|
204
|
+
const [success, result] = pcall(() => {
|
|
205
|
+
const hadTag = CollectionService.HasTag(instance, tagName);
|
|
206
|
+
CollectionService.RemoveTag(instance, tagName);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
success: true, instancePath, tagName, hadTag,
|
|
210
|
+
message: hadTag ? "Tag removed successfully" : "Instance did not have this tag",
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (success) {
|
|
215
|
+
finishRecording(recordingId, true);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
finishRecording(recordingId, false);
|
|
219
|
+
return { error: `Failed to remove tag: ${result}` };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getTagged(requestData: Record<string, unknown>) {
|
|
223
|
+
const tagName = requestData.tagName as string;
|
|
224
|
+
if (!tagName) return { error: "Tag name is required" };
|
|
225
|
+
|
|
226
|
+
const [success, result] = pcall(() => {
|
|
227
|
+
const taggedInstances = CollectionService.GetTagged(tagName);
|
|
228
|
+
const instances = taggedInstances.map((instance) => ({
|
|
229
|
+
name: instance.Name,
|
|
230
|
+
className: instance.ClassName,
|
|
231
|
+
path: getInstancePath(instance),
|
|
232
|
+
}));
|
|
233
|
+
|
|
234
|
+
return { tagName, instances, count: instances.size() };
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (success) return result;
|
|
238
|
+
return { error: `Failed to get tagged instances: ${result}` };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getSelection(_requestData: Record<string, unknown>) {
|
|
242
|
+
const selection = Selection.Get();
|
|
243
|
+
|
|
244
|
+
if (selection.size() === 0) {
|
|
245
|
+
return { success: true, selection: [], count: 0, message: "No objects selected" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const selectedObjects = selection.map((instance: Instance) => ({
|
|
249
|
+
name: instance.Name,
|
|
250
|
+
className: instance.ClassName,
|
|
251
|
+
path: getInstancePath(instance),
|
|
252
|
+
parent: instance.Parent ? getInstancePath(instance.Parent) : undefined,
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
success: true,
|
|
257
|
+
selection: selectedObjects,
|
|
258
|
+
count: selection.size(),
|
|
259
|
+
message: `${selection.size()} object(s) selected`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function executeLuau(requestData: Record<string, unknown>) {
|
|
264
|
+
const code = requestData.code as string;
|
|
265
|
+
if (!code || code === "") return { error: "Code is required" };
|
|
266
|
+
// All wrapping, print/warn capture, loadstring fallback, JSON-encoding
|
|
267
|
+
// of table returns, and parse-error recovery live in LuauExec so the
|
|
268
|
+
// edit/server (this handler) and the play-client (ClientBroker) take
|
|
269
|
+
// the same code path and produce identical output shapes.
|
|
270
|
+
return LuauExec.execute(code);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function undo(_requestData: Record<string, unknown>) {
|
|
274
|
+
const [success, result] = pcall(() => {
|
|
275
|
+
ChangeHistoryService.Undo();
|
|
276
|
+
return {
|
|
277
|
+
success: true,
|
|
278
|
+
message: "Undo executed successfully",
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (success) return result;
|
|
283
|
+
return { error: `Failed to undo: ${result}` };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function redo(_requestData: Record<string, unknown>) {
|
|
287
|
+
const [success, result] = pcall(() => {
|
|
288
|
+
ChangeHistoryService.Redo();
|
|
289
|
+
return {
|
|
290
|
+
success: true,
|
|
291
|
+
message: "Redo executed successfully",
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (success) return result;
|
|
296
|
+
return { error: `Failed to redo: ${result}` };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function bulkSetAttributes(requestData: Record<string, unknown>) {
|
|
300
|
+
const instancePath = requestData.instancePath as string;
|
|
301
|
+
const attributes = requestData.attributes as Record<string, unknown>;
|
|
302
|
+
|
|
303
|
+
if (!instancePath || !attributes) {
|
|
304
|
+
return { error: "Instance path and attributes are required" };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const instance = getInstanceByPath(instancePath);
|
|
308
|
+
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
309
|
+
|
|
310
|
+
const recordingId = beginRecording(`Bulk set attributes on ${instance.Name}`);
|
|
311
|
+
|
|
312
|
+
const results: Record<string, unknown>[] = [];
|
|
313
|
+
let successCount = 0;
|
|
314
|
+
let failureCount = 0;
|
|
315
|
+
|
|
316
|
+
for (const [name, rawValue] of pairs(attributes)) {
|
|
317
|
+
const attrName = name as string;
|
|
318
|
+
const [ok, err] = pcall(() => {
|
|
319
|
+
const value = deserializeValue(rawValue);
|
|
320
|
+
instance.SetAttribute(attrName, value as AttributeValue);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (ok) {
|
|
324
|
+
successCount++;
|
|
325
|
+
results.push({ attributeName: attrName, success: true });
|
|
326
|
+
} else {
|
|
327
|
+
failureCount++;
|
|
328
|
+
results.push({ attributeName: attrName, success: false, error: tostring(err) });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
finishRecording(recordingId, successCount > 0);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
instancePath,
|
|
336
|
+
results,
|
|
337
|
+
summary: { total: successCount + failureCount, succeeded: successCount, failed: failureCount },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export = {
|
|
342
|
+
setAttribute,
|
|
343
|
+
getAttributes,
|
|
344
|
+
deleteAttribute,
|
|
345
|
+
getTags,
|
|
346
|
+
addTag,
|
|
347
|
+
removeTag,
|
|
348
|
+
getTagged,
|
|
349
|
+
getSelection,
|
|
350
|
+
executeLuau,
|
|
351
|
+
undo,
|
|
352
|
+
redo,
|
|
353
|
+
bulkSetAttributes,
|
|
354
|
+
};
|