@chrrxs/robloxstudio-mcp 2.23.1 → 3.0.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 +1409 -4434
- package/package.json +6 -4
- package/studio-plugin/MCPInspectorPlugin.rbxmx +185 -1953
- package/studio-plugin/MCPPlugin.rbxmx +185 -1953
- package/studio-plugin/src/modules/Communication.ts +16 -47
- package/studio-plugin/src/modules/Utils.ts +60 -2
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +3 -261
- package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +1 -130
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +9 -167
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +64 -63
- package/studio-plugin/src/modules/handlers/BuildHandlers.ts +0 -481
- package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +0 -380
|
@@ -5,11 +5,9 @@ import UI from "./UI";
|
|
|
5
5
|
import { cleanupLegacyEditBridges } from "./EvalBridges";
|
|
6
6
|
import QueryHandlers from "./handlers/QueryHandlers";
|
|
7
7
|
import PropertyHandlers from "./handlers/PropertyHandlers";
|
|
8
|
-
import InstanceHandlers from "./handlers/InstanceHandlers";
|
|
9
8
|
import ScriptHandlers from "./handlers/ScriptHandlers";
|
|
10
9
|
import MetadataHandlers from "./handlers/MetadataHandlers";
|
|
11
10
|
import TestHandlers from "./handlers/TestHandlers";
|
|
12
|
-
import BuildHandlers from "./handlers/BuildHandlers";
|
|
13
11
|
import AssetHandlers from "./handlers/AssetHandlers";
|
|
14
12
|
import CaptureHandlers from "./handlers/CaptureHandlers";
|
|
15
13
|
import InputHandlers from "./handlers/InputHandlers";
|
|
@@ -100,32 +98,17 @@ type Handler = (data: Record<string, unknown>) => unknown;
|
|
|
100
98
|
|
|
101
99
|
const routeMap: Record<string, Handler> = {
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"/api/get-descendants": QueryHandlers.getDescendants,
|
|
115
|
-
"/api/compare-instances": QueryHandlers.compareInstances,
|
|
116
|
-
|
|
117
|
-
"/api/set-property": PropertyHandlers.setProperty,
|
|
118
|
-
"/api/set-properties": PropertyHandlers.setProperties,
|
|
119
|
-
"/api/mass-set-property": PropertyHandlers.massSetProperty,
|
|
120
|
-
"/api/mass-get-property": PropertyHandlers.massGetProperty,
|
|
121
|
-
"/api/create-object": InstanceHandlers.createObject,
|
|
122
|
-
"/api/mass-create-objects": InstanceHandlers.massCreateObjects,
|
|
123
|
-
// Back-compat alias: pre-2.7.0 servers split this endpoint when properties were present.
|
|
124
|
-
"/api/mass-create-objects-with-properties": InstanceHandlers.massCreateObjects,
|
|
125
|
-
"/api/delete-object": InstanceHandlers.deleteObject,
|
|
126
|
-
"/api/smart-duplicate": InstanceHandlers.smartDuplicate,
|
|
127
|
-
"/api/mass-duplicate": InstanceHandlers.massDuplicate,
|
|
128
|
-
"/api/clone-object": InstanceHandlers.cloneObject,
|
|
101
|
+
"/api/file-tree": QueryHandlers.getFileTree,
|
|
102
|
+
"/api/search-files": QueryHandlers.searchFiles,
|
|
103
|
+
"/api/place-info": QueryHandlers.getPlaceInfo,
|
|
104
|
+
"/api/search-objects": QueryHandlers.searchObjects,
|
|
105
|
+
"/api/instance-properties": QueryHandlers.getInstanceProperties,
|
|
106
|
+
"/api/search-by-property": QueryHandlers.searchByProperty,
|
|
107
|
+
"/api/class-info": QueryHandlers.getClassInfo,
|
|
108
|
+
"/api/project-structure": QueryHandlers.getProjectStructure,
|
|
109
|
+
"/api/grep-scripts": QueryHandlers.grepScripts,
|
|
110
|
+
|
|
111
|
+
"/api/set-properties": PropertyHandlers.setProperties,
|
|
129
112
|
|
|
130
113
|
"/api/get-script-source": ScriptHandlers.getScriptSource,
|
|
131
114
|
"/api/set-script-source": ScriptHandlers.setScriptSource,
|
|
@@ -133,19 +116,10 @@ const routeMap: Record<string, Handler> = {
|
|
|
133
116
|
"/api/insert-script-lines": ScriptHandlers.insertScriptLines,
|
|
134
117
|
"/api/delete-script-lines": ScriptHandlers.deleteScriptLines,
|
|
135
118
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"/api/add-tag": MetadataHandlers.addTag,
|
|
141
|
-
"/api/remove-tag": MetadataHandlers.removeTag,
|
|
142
|
-
"/api/get-tagged": MetadataHandlers.getTagged,
|
|
143
|
-
"/api/get-selection": MetadataHandlers.getSelection,
|
|
144
|
-
"/api/execute-luau": MetadataHandlers.executeLuau,
|
|
145
|
-
"/api/eval-runtime": EvalRuntimeHandlers.evalRuntime,
|
|
146
|
-
"/api/undo": MetadataHandlers.undo,
|
|
147
|
-
"/api/redo": MetadataHandlers.redo,
|
|
148
|
-
"/api/bulk-set-attributes": MetadataHandlers.bulkSetAttributes,
|
|
119
|
+
"/api/get-attributes": MetadataHandlers.getAttributes,
|
|
120
|
+
"/api/get-selection": MetadataHandlers.getSelection,
|
|
121
|
+
"/api/execute-luau": MetadataHandlers.executeLuau,
|
|
122
|
+
"/api/eval-runtime": EvalRuntimeHandlers.evalRuntime,
|
|
149
123
|
|
|
150
124
|
"/api/start-playtest": TestHandlers.startPlaytest,
|
|
151
125
|
"/api/stop-playtest": TestHandlers.stopPlaytest,
|
|
@@ -155,12 +129,7 @@ const routeMap: Record<string, Handler> = {
|
|
|
155
129
|
"/api/multiplayer-test-leave-client": TestHandlers.multiplayerTestLeaveClient,
|
|
156
130
|
"/api/multiplayer-test-end": TestHandlers.multiplayerTestEnd,
|
|
157
131
|
|
|
158
|
-
|
|
159
|
-
"/api/import-build": BuildHandlers.importBuild,
|
|
160
|
-
"/api/import-scene": BuildHandlers.importScene,
|
|
161
|
-
"/api/search-materials": BuildHandlers.searchMaterials,
|
|
162
|
-
|
|
163
|
-
"/api/insert-asset": AssetHandlers.insertAsset,
|
|
132
|
+
"/api/insert-asset": AssetHandlers.insertAsset,
|
|
164
133
|
"/api/preview-asset": AssetHandlers.previewAsset,
|
|
165
134
|
|
|
166
135
|
"/api/capture-screenshot": CaptureHandlers.captureScreenshot,
|
|
@@ -224,10 +224,67 @@ function readScriptSource(instance: LuaSourceContainer): string {
|
|
|
224
224
|
}
|
|
225
225
|
return undefined;
|
|
226
226
|
});
|
|
227
|
-
if (ok && result) {
|
|
227
|
+
if (ok && result !== undefined) {
|
|
228
228
|
return result;
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
// @rbxts/types does not expose PluginSecurity Source reads.
|
|
231
|
+
const readableScript = instance as unknown as { Source: string };
|
|
232
|
+
return readableScript.Source;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface ApplyScriptSourceResult {
|
|
236
|
+
success: boolean;
|
|
237
|
+
method: "UpdateSourceAsync" | "direct";
|
|
238
|
+
error?: string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Writes newSource to instance and verifies the live editor text before
|
|
243
|
+
* reporting success. expectedSource turns line-based edits into a
|
|
244
|
+
* compare-and-set operation: if the document changes while
|
|
245
|
+
* UpdateSourceAsync yields or before the direct fallback runs, the write is
|
|
246
|
+
* rejected instead of overwriting the newer draft.
|
|
247
|
+
*/
|
|
248
|
+
function applyScriptSource(
|
|
249
|
+
instance: LuaSourceContainer,
|
|
250
|
+
newSource: string,
|
|
251
|
+
expectedSource?: string,
|
|
252
|
+
): ApplyScriptSourceResult {
|
|
253
|
+
const [updateSuccess, updateResult] = pcall(() => {
|
|
254
|
+
ScriptEditorService.UpdateSourceAsync(instance, (currentSource: string) => {
|
|
255
|
+
if (expectedSource !== undefined && currentSource !== expectedSource) {
|
|
256
|
+
error("Script source changed while the edit was being applied; read the script again and retry");
|
|
257
|
+
}
|
|
258
|
+
return newSource;
|
|
259
|
+
});
|
|
260
|
+
if (readScriptSource(instance) !== newSource) {
|
|
261
|
+
error("UpdateSourceAsync completed without updating the live script source");
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
if (updateSuccess) {
|
|
265
|
+
return { success: true, method: "UpdateSourceAsync" };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const [directSuccess, directResult] = pcall(() => {
|
|
269
|
+
if (expectedSource !== undefined && readScriptSource(instance) !== expectedSource) {
|
|
270
|
+
error("Script source changed before direct assignment; read the script again and retry");
|
|
271
|
+
}
|
|
272
|
+
// @rbxts/types does not expose PluginSecurity Source writes.
|
|
273
|
+
const writableScript = instance as unknown as { Source: string };
|
|
274
|
+
writableScript.Source = newSource;
|
|
275
|
+
if (readScriptSource(instance) !== newSource) {
|
|
276
|
+
error("Direct assignment completed without updating the live script source");
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
if (directSuccess) {
|
|
280
|
+
return { success: true, method: "direct" };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
method: "direct",
|
|
286
|
+
error: `UpdateSourceAsync failed: ${updateResult}. Direct assignment failed: ${directResult}`,
|
|
287
|
+
};
|
|
231
288
|
}
|
|
232
289
|
|
|
233
290
|
function convertPropertyValue(instance: Instance, propertyName: string, propertyValue: unknown): unknown {
|
|
@@ -463,6 +520,7 @@ export = {
|
|
|
463
520
|
splitLines,
|
|
464
521
|
joinLines,
|
|
465
522
|
readScriptSource,
|
|
523
|
+
applyScriptSource,
|
|
466
524
|
convertPropertyValue,
|
|
467
525
|
evaluateFormula,
|
|
468
526
|
compareVersions,
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import { CollectionService } from "@rbxts/services";
|
|
2
1
|
import Utils from "../Utils";
|
|
3
|
-
import Recording from "../Recording";
|
|
4
2
|
import LuauExec from "../LuauExec";
|
|
5
3
|
|
|
6
|
-
const ChangeHistoryService = game.GetService("ChangeHistoryService");
|
|
7
4
|
const Selection = game.GetService("Selection");
|
|
8
5
|
|
|
9
6
|
const { getInstancePath, getInstanceByPath } = Utils;
|
|
10
|
-
const { beginRecording, finishRecording } = Recording;
|
|
11
7
|
|
|
12
8
|
function serializeValue(value: unknown): unknown {
|
|
13
9
|
const vType = typeOf(value);
|
|
@@ -34,58 +30,6 @@ function serializeValue(value: unknown): unknown {
|
|
|
34
30
|
return value;
|
|
35
31
|
}
|
|
36
32
|
|
|
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
33
|
function getAttributes(requestData: Record<string, unknown>) {
|
|
90
34
|
const instancePath = requestData.instancePath as string;
|
|
91
35
|
if (!instancePath) return { error: "Instance path is required" };
|
|
@@ -113,131 +57,6 @@ function getAttributes(requestData: Record<string, unknown>) {
|
|
|
113
57
|
return { error: `Failed to get attributes: ${result}` };
|
|
114
58
|
}
|
|
115
59
|
|
|
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
60
|
function getSelection(_requestData: Record<string, unknown>) {
|
|
242
61
|
const selection = Selection.Get();
|
|
243
62
|
|
|
@@ -270,85 +89,8 @@ function executeLuau(requestData: Record<string, unknown>) {
|
|
|
270
89
|
return LuauExec.execute(code);
|
|
271
90
|
}
|
|
272
91
|
|
|
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
92
|
export = {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
getTags,
|
|
346
|
-
addTag,
|
|
347
|
-
removeTag,
|
|
348
|
-
getTagged,
|
|
349
|
-
getSelection,
|
|
350
|
-
executeLuau,
|
|
351
|
-
undo,
|
|
352
|
-
redo,
|
|
353
|
-
bulkSetAttributes,
|
|
93
|
+
getAttributes,
|
|
94
|
+
getSelection,
|
|
95
|
+
executeLuau,
|
|
354
96
|
};
|
|
@@ -4,132 +4,6 @@ import Recording from "../Recording";
|
|
|
4
4
|
const { getInstanceByPath, convertPropertyValue } = Utils;
|
|
5
5
|
const { beginRecording, finishRecording } = Recording;
|
|
6
6
|
|
|
7
|
-
function setProperty(requestData: Record<string, unknown>) {
|
|
8
|
-
const instancePath = requestData.instancePath as string;
|
|
9
|
-
const propertyName = requestData.propertyName as string;
|
|
10
|
-
const propertyValue = requestData.propertyValue;
|
|
11
|
-
|
|
12
|
-
if (!instancePath || !propertyName) {
|
|
13
|
-
return { error: "Instance path and property name are required" };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const instance = getInstanceByPath(instancePath);
|
|
17
|
-
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
18
|
-
const recordingId = beginRecording(`Set ${propertyName} property`);
|
|
19
|
-
|
|
20
|
-
const inst = instance as unknown as Record<string, unknown>;
|
|
21
|
-
|
|
22
|
-
const [success, result] = pcall(() => {
|
|
23
|
-
if (propertyName === "Parent" || propertyName === "PrimaryPart") {
|
|
24
|
-
if (typeIs(propertyValue, "string")) {
|
|
25
|
-
const refInstance = getInstanceByPath(propertyValue);
|
|
26
|
-
if (refInstance) {
|
|
27
|
-
inst[propertyName] = refInstance;
|
|
28
|
-
} else {
|
|
29
|
-
return { error: `${propertyName} instance not found: ${propertyValue}` };
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
} else if (propertyName === "Name") {
|
|
33
|
-
instance.Name = tostring(propertyValue);
|
|
34
|
-
} else if (propertyName === "Source" && instance.IsA("LuaSourceContainer")) {
|
|
35
|
-
(instance as unknown as { Source: string }).Source = tostring(propertyValue);
|
|
36
|
-
} else {
|
|
37
|
-
const convertedValue = convertPropertyValue(instance, propertyName, propertyValue);
|
|
38
|
-
if (convertedValue !== undefined) {
|
|
39
|
-
inst[propertyName] = convertedValue;
|
|
40
|
-
} else {
|
|
41
|
-
inst[propertyName] = propertyValue;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return true;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (success) {
|
|
49
|
-
finishRecording(recordingId, true);
|
|
50
|
-
return {
|
|
51
|
-
success: true,
|
|
52
|
-
instancePath,
|
|
53
|
-
propertyName,
|
|
54
|
-
propertyValue,
|
|
55
|
-
message: "Property set successfully",
|
|
56
|
-
};
|
|
57
|
-
} else {
|
|
58
|
-
finishRecording(recordingId, false);
|
|
59
|
-
return { error: `Failed to set property: ${result}`, instancePath, propertyName };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function massSetProperty(requestData: Record<string, unknown>) {
|
|
64
|
-
const paths = requestData.paths as string[];
|
|
65
|
-
const propertyName = requestData.propertyName as string;
|
|
66
|
-
const propertyValue = requestData.propertyValue;
|
|
67
|
-
|
|
68
|
-
if (!paths || !typeIs(paths, "table") || (paths as defined[]).size() === 0 || !propertyName) {
|
|
69
|
-
return { error: "Paths array and property name are required" };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const results: Record<string, unknown>[] = [];
|
|
73
|
-
let successCount = 0;
|
|
74
|
-
let failureCount = 0;
|
|
75
|
-
const recordingId = beginRecording(`Mass set ${propertyName} property`);
|
|
76
|
-
|
|
77
|
-
for (const path of paths) {
|
|
78
|
-
const instance = getInstanceByPath(path);
|
|
79
|
-
if (instance) {
|
|
80
|
-
const [success, err] = pcall(() => {
|
|
81
|
-
const converted = convertPropertyValue(instance, propertyName, propertyValue);
|
|
82
|
-
(instance as unknown as Record<string, unknown>)[propertyName] =
|
|
83
|
-
converted !== undefined ? converted : propertyValue;
|
|
84
|
-
});
|
|
85
|
-
if (success) {
|
|
86
|
-
successCount++;
|
|
87
|
-
results.push({ path, success: true, propertyName, propertyValue });
|
|
88
|
-
} else {
|
|
89
|
-
failureCount++;
|
|
90
|
-
results.push({ path, success: false, error: tostring(err) });
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
failureCount++;
|
|
94
|
-
results.push({ path, success: false, error: "Instance not found" });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
finishRecording(recordingId, successCount > 0);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
results,
|
|
102
|
-
summary: { total: paths.size(), succeeded: successCount, failed: failureCount },
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function massGetProperty(requestData: Record<string, unknown>) {
|
|
107
|
-
const paths = requestData.paths as string[];
|
|
108
|
-
const propertyName = requestData.propertyName as string;
|
|
109
|
-
|
|
110
|
-
if (!paths || !typeIs(paths, "table") || (paths as defined[]).size() === 0 || !propertyName) {
|
|
111
|
-
return { error: "Paths array and property name are required" };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const results: Record<string, unknown>[] = [];
|
|
115
|
-
|
|
116
|
-
for (const path of paths) {
|
|
117
|
-
const instance = getInstanceByPath(path);
|
|
118
|
-
if (instance) {
|
|
119
|
-
const [success, value] = pcall(() => (instance as unknown as Record<string, unknown>)[propertyName]);
|
|
120
|
-
if (success) {
|
|
121
|
-
results.push({ path, success: true, propertyName, propertyValue: value });
|
|
122
|
-
} else {
|
|
123
|
-
results.push({ path, success: false, error: tostring(value) });
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
results.push({ path, success: false, error: "Instance not found" });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return { results, propertyName };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
7
|
function setProperties(requestData: Record<string, unknown>) {
|
|
134
8
|
const instancePath = requestData.instancePath as string;
|
|
135
9
|
const properties = requestData.properties as Record<string, unknown>;
|
|
@@ -184,8 +58,5 @@ function setProperties(requestData: Record<string, unknown>) {
|
|
|
184
58
|
}
|
|
185
59
|
|
|
186
60
|
export = {
|
|
187
|
-
|
|
188
|
-
massSetProperty,
|
|
189
|
-
massGetProperty,
|
|
190
|
-
setProperties,
|
|
61
|
+
setProperties,
|
|
191
62
|
};
|