robloxstudio-mcp 2.4.0 → 2.5.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.
@@ -10,6 +10,7 @@ import MetadataHandlers from "./handlers/MetadataHandlers";
10
10
  import TestHandlers from "./handlers/TestHandlers";
11
11
  import BuildHandlers from "./handlers/BuildHandlers";
12
12
  import AssetHandlers from "./handlers/AssetHandlers";
13
+ import CaptureHandlers from "./handlers/CaptureHandlers";
13
14
  import { Connection, RequestPayload, PollResponse } from "../types";
14
15
 
15
16
  type Handler = (data: Record<string, unknown>) => unknown;
@@ -71,6 +72,8 @@ const routeMap: Record<string, Handler> = {
71
72
 
72
73
  "/api/insert-asset": AssetHandlers.insertAsset,
73
74
  "/api/preview-asset": AssetHandlers.previewAsset,
75
+
76
+ "/api/capture-screenshot": CaptureHandlers.captureScreenshot,
74
77
  };
75
78
 
76
79
  function processRequest(request: RequestPayload): unknown {
@@ -39,8 +39,8 @@ function getInstanceByPath(path: string): Instance | undefined {
39
39
 
40
40
  let current: Instance | undefined = game;
41
41
  for (const part of parts) {
42
- current = current?.FindFirstChild(part);
43
42
  if (!current) return undefined;
43
+ current = current.FindFirstChild(part);
44
44
  }
45
45
 
46
46
  return current;
@@ -24,67 +24,69 @@ function insertAsset(requestData: Record<string, unknown>) {
24
24
 
25
25
  const recordingId = beginRecording(`Insert asset ${assetId}`);
26
26
 
27
- const [loadSuccess, wrapperModel] = pcall(() => {
28
- return (AssetService as unknown as { LoadAssetAsync(id: number): Instance }).LoadAssetAsync(assetId);
29
- });
30
-
31
- if (!loadSuccess || !wrapperModel) {
32
- finishRecording(recordingId, false);
33
- return { error: `Failed to load asset ${assetId}: ${tostring(wrapperModel)}` };
34
- }
27
+ let wrapperModel: Instance | undefined;
28
+ const [insertSuccess, insertResult] = pcall(() => {
29
+ const loadedWrapper = (AssetService as unknown as { LoadAssetAsync(id: number): Instance }).LoadAssetAsync(assetId);
30
+ wrapperModel = loadedWrapper;
35
31
 
36
- const insertedInstances: Instance[] = [];
37
- const children = (wrapperModel as Instance).GetChildren();
32
+ const insertedInstances: Instance[] = [];
33
+ const children = loadedWrapper.GetChildren();
38
34
 
39
- for (const child of children) {
40
- child.Parent = parentInstance;
41
- insertedInstances.push(child);
42
- }
35
+ for (const child of children) {
36
+ child.Parent = parentInstance;
37
+ insertedInstances.push(child);
38
+ }
43
39
 
44
- // Apply position if provided
45
- if (position) {
46
- const pos = new Vector3(position.x ?? 0, position.y ?? 0, position.z ?? 0);
47
- for (const inst of insertedInstances) {
48
- if (inst.IsA("BasePart")) {
49
- inst.Position = pos;
50
- } else if (inst.IsA("Model")) {
51
- if (inst.PrimaryPart) {
52
- inst.PivotTo(new CFrame(pos));
53
- } else {
54
- // Find first BasePart descendant to pivot around
55
- const firstPart = inst.FindFirstChildWhichIsA("BasePart", true);
56
- if (firstPart) {
40
+ if (position) {
41
+ const pos = new Vector3(position.x ?? 0, position.y ?? 0, position.z ?? 0);
42
+ for (const inst of insertedInstances) {
43
+ if (inst.IsA("BasePart")) {
44
+ inst.Position = pos;
45
+ } else if (inst.IsA("Model")) {
46
+ if (inst.PrimaryPart) {
57
47
  inst.PivotTo(new CFrame(pos));
48
+ } else {
49
+ const firstPart = inst.FindFirstChildWhichIsA("BasePart", true);
50
+ if (firstPart) {
51
+ inst.PivotTo(new CFrame(pos));
52
+ }
58
53
  }
59
54
  }
60
55
  }
61
56
  }
62
- }
63
57
 
64
- // Destroy the wrapper model
65
- (wrapperModel as Instance).Destroy();
58
+ pcall(() => {
59
+ Selection.Set(insertedInstances);
60
+ });
61
+
62
+ const resultInstances = insertedInstances.map((inst) => ({
63
+ name: inst.Name,
64
+ className: inst.ClassName,
65
+ path: getInstancePath(inst),
66
+ }));
67
+
68
+ return {
69
+ success: true,
70
+ assetId,
71
+ parentPath,
72
+ insertedCount: insertedInstances.size(),
73
+ instances: resultInstances,
74
+ };
75
+ });
66
76
 
67
- // Set undo waypoint
68
- finishRecording(recordingId, true);
77
+ if (wrapperModel) {
78
+ pcall(() => {
79
+ wrapperModel!.Destroy();
80
+ });
81
+ }
69
82
 
70
- // Select inserted instances
71
- pcall(() => {
72
- Selection.Set(insertedInstances);
73
- });
83
+ finishRecording(recordingId, insertSuccess);
84
+
85
+ if (!insertSuccess) {
86
+ return { error: `Failed to insert asset ${assetId}: ${tostring(insertResult)}` };
87
+ }
74
88
 
75
- const resultInstances = insertedInstances.map((inst) => ({
76
- name: inst.Name,
77
- className: inst.ClassName,
78
- path: getInstancePath(inst),
79
- }));
80
-
81
- return {
82
- success: true,
83
- assetId,
84
- parentPath,
85
- insertedCount: insertedInstances.size(),
86
- instances: resultInstances,
87
- };
89
+ return insertResult;
88
90
  }
89
91
 
90
92
  function previewAsset(requestData: Record<string, unknown>) {
@@ -201,27 +203,36 @@ function previewAsset(requestData: Record<string, unknown>) {
201
203
  return node;
202
204
  }
203
205
 
204
- const hierarchyRoots: Record<string, unknown>[] = [];
205
- for (const child of (wrapperModel as Instance).GetChildren()) {
206
- hierarchyRoots.push(buildHierarchy(child, 0));
206
+ const [previewSuccess, previewResult] = pcall(() => {
207
+ const hierarchyRoots: Record<string, unknown>[] = [];
208
+ for (const child of (wrapperModel as Instance).GetChildren()) {
209
+ hierarchyRoots.push(buildHierarchy(child, 0));
210
+ }
211
+
212
+ return {
213
+ success: true,
214
+ assetId,
215
+ hierarchy: hierarchyRoots,
216
+ summary: {
217
+ totalInstances,
218
+ classCounts,
219
+ hasScripts,
220
+ hasAnimations,
221
+ hasSounds,
222
+ hasParticles,
223
+ },
224
+ };
225
+ });
226
+
227
+ pcall(() => {
228
+ (wrapperModel as Instance).Destroy();
229
+ });
230
+
231
+ if (!previewSuccess) {
232
+ return { error: `Failed to preview asset ${assetId}: ${tostring(previewResult)}` };
207
233
  }
208
234
 
209
- // CRITICAL: Destroy wrapper to prevent memory leak
210
- (wrapperModel as Instance).Destroy();
211
-
212
- return {
213
- success: true,
214
- assetId,
215
- hierarchy: hierarchyRoots,
216
- summary: {
217
- totalInstances,
218
- classCounts,
219
- hasScripts,
220
- hasAnimations,
221
- hasSounds,
222
- hasParticles,
223
- },
224
- };
235
+ return previewResult;
225
236
  }
226
237
 
227
238
  export = {
@@ -6,30 +6,10 @@ const MaterialService = game.GetService("MaterialService");
6
6
  const { getInstancePath, getInstanceByPath } = Utils;
7
7
  const { beginRecording, finishRecording } = Recording;
8
8
 
9
- // Material name lookup table
10
- const MATERIAL_NAMES: Record<string, Enum.Material> = {
11
- Plastic: Enum.Material.Plastic,
12
- Wood: Enum.Material.Wood,
13
- WoodPlanks: Enum.Material.WoodPlanks,
14
- Slate: Enum.Material.Slate,
15
- Concrete: Enum.Material.Concrete,
16
- CorrodedMetal: Enum.Material.CorrodedMetal,
17
- DiamondPlate: Enum.Material.DiamondPlate,
18
- Foil: Enum.Material.Foil,
19
- Grass: Enum.Material.Grass,
20
- Ice: Enum.Material.Ice,
21
- Marble: Enum.Material.Marble,
22
- Granite: Enum.Material.Granite,
23
- Brick: Enum.Material.Brick,
24
- Pebble: Enum.Material.Pebble,
25
- Sand: Enum.Material.Sand,
26
- Fabric: Enum.Material.Fabric,
27
- SmoothPlastic: Enum.Material.SmoothPlastic,
28
- Metal: Enum.Material.Metal,
29
- Neon: Enum.Material.Neon,
30
- Glass: Enum.Material.Glass,
31
- Cobblestone: Enum.Material.Cobblestone,
32
- };
9
+ const MATERIAL_BY_NAME = new Map<string, Enum.Material>();
10
+ for (const enumItem of Enum.Material.GetEnumItems()) {
11
+ MATERIAL_BY_NAME.set(enumItem.Name, enumItem as unknown as Enum.Material);
12
+ }
33
13
 
34
14
  // Shape class mapping
35
15
  const SHAPE_CLASSES: Record<string, string> = {
@@ -47,6 +27,30 @@ function roundTo(n: number, decimals: number): number {
47
27
  return math.round(n * mult) / mult;
48
28
  }
49
29
 
30
+ function encodePaletteKey(index: number): string {
31
+ const base = PALETTE_KEYS.size();
32
+ let value = math.floor(index) + 1;
33
+ let encoded = "";
34
+ while (value > 0) {
35
+ value -= 1;
36
+ const digit = value % base;
37
+ encoded = PALETTE_KEYS.sub(digit + 1, digit + 1) + encoded;
38
+ value = math.floor(value / base);
39
+ }
40
+ return encoded;
41
+ }
42
+
43
+ function getVariantName(part: BasePart): string {
44
+ let variantName = part.MaterialVariant;
45
+ if (variantName === "") {
46
+ const [ok, variantAttr] = pcall(() => part.GetAttribute("MaterialVariant"));
47
+ if (ok && type(variantAttr) === "string") {
48
+ variantName = variantAttr as string;
49
+ }
50
+ }
51
+ return variantName;
52
+ }
53
+
50
54
  function exportBuild(requestData: Record<string, unknown>) {
51
55
  const instancePath = requestData.instancePath as string;
52
56
  const outputId = requestData.outputId as string;
@@ -113,11 +117,11 @@ function exportBuild(requestData: Record<string, unknown>) {
113
117
  for (const part of baseParts) {
114
118
  const colorName = part.BrickColor.Name;
115
119
  const materialName = part.Material.Name;
116
- const variantName = part.MaterialVariant;
120
+ const variantName = getVariantName(part);
117
121
  const combo = variantName !== "" ? `${colorName}|${materialName}|${variantName}` : `${colorName}|${materialName}`;
118
122
 
119
123
  if (!paletteMap.has(combo)) {
120
- const key = PALETTE_KEYS.sub(keyIndex + 1, keyIndex + 1);
124
+ const key = encodePaletteKey(keyIndex);
121
125
  keyIndex++;
122
126
  paletteMap.set(combo, key);
123
127
  if (variantName !== "") {
@@ -136,7 +140,8 @@ function exportBuild(requestData: Record<string, unknown>) {
136
140
  const sz = part.Size;
137
141
  const colorName = part.BrickColor.Name;
138
142
  const materialName = part.Material.Name;
139
- const combo = `${colorName}|${materialName}`;
143
+ const variantName = getVariantName(part);
144
+ const combo = variantName !== "" ? `${colorName}|${materialName}|${variantName}` : `${colorName}|${materialName}`;
140
145
  const paletteKey = paletteMap.get(combo) ?? "a";
141
146
 
142
147
  // Relative position to center
@@ -270,8 +275,8 @@ function importBuild(requestData: Record<string, unknown>) {
270
275
  part.BrickColor = new BrickColor(colorName as unknown as number);
271
276
  });
272
277
  pcall(() => {
273
- const mat = MATERIAL_NAMES[materialName];
274
- if (mat) {
278
+ const mat = MATERIAL_BY_NAME.get(materialName);
279
+ if (mat !== undefined) {
275
280
  part.Material = mat;
276
281
  }
277
282
  });
@@ -389,8 +394,8 @@ function importScene(requestData: Record<string, unknown>) {
389
394
  part.BrickColor = new BrickColor(colorName as unknown as number);
390
395
  });
391
396
  pcall(() => {
392
- const mat = MATERIAL_NAMES[materialName];
393
- if (mat) {
397
+ const mat = MATERIAL_BY_NAME.get(materialName);
398
+ if (mat !== undefined) {
394
399
  part.Material = mat;
395
400
  }
396
401
  });
@@ -0,0 +1,118 @@
1
+ const CaptureService = game.GetService("CaptureService");
2
+ const AssetService = game.GetService("AssetService");
3
+
4
+ const MAX_TILE_SIZE = 1024;
5
+ const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
6
+
7
+ function encodeBase64(buf: buffer): string {
8
+ const len = buffer.len(buf);
9
+ const parts: string[] = [];
10
+ let i = 0;
11
+
12
+ while (i + 2 < len) {
13
+ const b0 = buffer.readu8(buf, i);
14
+ const b1 = buffer.readu8(buf, i + 1);
15
+ const b2 = buffer.readu8(buf, i + 2);
16
+ const triplet = bit32.lshift(b0, 16) + bit32.lshift(b1, 8) + b2;
17
+ parts.push(
18
+ string.sub(BASE64_CHARS, bit32.rshift(triplet, 18) + 1, bit32.rshift(triplet, 18) + 1) +
19
+ string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 12), 63) + 1, bit32.band(bit32.rshift(triplet, 12), 63) + 1) +
20
+ string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 6), 63) + 1, bit32.band(bit32.rshift(triplet, 6), 63) + 1) +
21
+ string.sub(BASE64_CHARS, bit32.band(triplet, 63) + 1, bit32.band(triplet, 63) + 1),
22
+ );
23
+ i += 3;
24
+ }
25
+
26
+ const remaining = len - i;
27
+ if (remaining === 2) {
28
+ const b0 = buffer.readu8(buf, i);
29
+ const b1 = buffer.readu8(buf, i + 1);
30
+ const triplet = bit32.lshift(b0, 16) + bit32.lshift(b1, 8);
31
+ parts.push(
32
+ string.sub(BASE64_CHARS, bit32.rshift(triplet, 18) + 1, bit32.rshift(triplet, 18) + 1) +
33
+ string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 12), 63) + 1, bit32.band(bit32.rshift(triplet, 12), 63) + 1) +
34
+ string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 6), 63) + 1, bit32.band(bit32.rshift(triplet, 6), 63) + 1) +
35
+ "=",
36
+ );
37
+ } else if (remaining === 1) {
38
+ const b0 = buffer.readu8(buf, i);
39
+ const triplet = bit32.lshift(b0, 16);
40
+ parts.push(
41
+ string.sub(BASE64_CHARS, bit32.rshift(triplet, 18) + 1, bit32.rshift(triplet, 18) + 1) +
42
+ string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 12), 63) + 1, bit32.band(bit32.rshift(triplet, 12), 63) + 1) +
43
+ "==",
44
+ );
45
+ }
46
+
47
+ return parts.join("");
48
+ }
49
+
50
+ function readPixelsTiled(img: EditableImage, w: number, h: number): buffer {
51
+ const BYTES_PER_PIXEL = 4;
52
+ const fullBuf = buffer.create(w * h * BYTES_PER_PIXEL);
53
+ const fullRowBytes = w * BYTES_PER_PIXEL;
54
+
55
+ for (let ty = 0; ty < h; ty += MAX_TILE_SIZE) {
56
+ const tileH = math.min(MAX_TILE_SIZE, h - ty);
57
+ for (let tx = 0; tx < w; tx += MAX_TILE_SIZE) {
58
+ const tileW = math.min(MAX_TILE_SIZE, w - tx);
59
+ const tileBuf = img.ReadPixelsBuffer(new Vector2(tx, ty), new Vector2(tileW, tileH));
60
+ const tileRowBytes = tileW * BYTES_PER_PIXEL;
61
+ for (let row = 0; row < tileH; row++) {
62
+ buffer.copy(fullBuf, (ty + row) * fullRowBytes + tx * BYTES_PER_PIXEL, tileBuf, row * tileRowBytes, tileRowBytes);
63
+ }
64
+ }
65
+ }
66
+ return fullBuf;
67
+ }
68
+
69
+ function captureScreenshot(): unknown {
70
+ let contentId: string | undefined;
71
+
72
+ CaptureService.CaptureScreenshot((id: string) => {
73
+ contentId = id;
74
+ });
75
+
76
+ const startTime = tick();
77
+ while (contentId === undefined) {
78
+ if (tick() - startTime > 10) {
79
+ return {
80
+ error: "Screenshot capture timed out. Ensure the Studio viewport is visible and you are in Edit mode (not Play mode). Known Roblox bug: capture may fail if viewport renders a solid color.",
81
+ };
82
+ }
83
+ task.wait(0.1);
84
+ }
85
+
86
+ const [editableOk, editableResult] = pcall(() => {
87
+ return AssetService.CreateEditableImageAsync(Content.fromUri(contentId!));
88
+ });
89
+
90
+ if (!editableOk) {
91
+ return {
92
+ error: `Failed to create EditableImage from screenshot. Enable EditableImage API: Game Settings > Security > 'Allow Mesh / Image APIs'. (${tostring(editableResult)})`,
93
+ };
94
+ }
95
+
96
+ const editableImage = editableResult as EditableImage;
97
+ const imgSize = editableImage.Size;
98
+ const w = math.floor(imgSize.X);
99
+ const h = math.floor(imgSize.Y);
100
+
101
+ const [readOk, pixelBuffer] = pcall(() => {
102
+ return readPixelsTiled(editableImage, w, h);
103
+ });
104
+
105
+ editableImage.Destroy();
106
+
107
+ if (!readOk) {
108
+ return { error: `Failed to read pixel data: ${tostring(pixelBuffer)}` };
109
+ }
110
+
111
+ const base64Data = encodeBase64(pixelBuffer as buffer);
112
+
113
+ return { success: true, width: w, height: h, data: base64Data };
114
+ }
115
+
116
+ export = {
117
+ captureScreenshot,
118
+ };
@@ -4,6 +4,86 @@ import Recording from "../Recording";
4
4
  const { getInstancePath, getInstanceByPath, convertPropertyValue } = Utils;
5
5
  const { beginRecording, finishRecording } = Recording;
6
6
 
7
+ type ProcessedCreateResult =
8
+ | {
9
+ instance: Instance;
10
+ className: string;
11
+ parentPath: string;
12
+ }
13
+ | {
14
+ error: string;
15
+ className?: string;
16
+ parentPath?: string;
17
+ };
18
+
19
+ type ProcessedBatchResult = {
20
+ results: Record<string, unknown>[];
21
+ successCount: number;
22
+ failureCount: number;
23
+ };
24
+
25
+ function processObjectEntries(
26
+ objects: Record<string, unknown>[],
27
+ createFn: (objData: Record<string, unknown>) => ProcessedCreateResult,
28
+ ): ProcessedBatchResult {
29
+ const results: Record<string, unknown>[] = [];
30
+ let successCount = 0;
31
+ let failureCount = 0;
32
+
33
+ const [loopSuccess, loopError] = pcall(() => {
34
+ for (const entry of objects) {
35
+ if (!typeIs(entry, "table")) {
36
+ failureCount++;
37
+ results.push({ success: false, error: "Each object entry must be a table" });
38
+ continue;
39
+ }
40
+
41
+ const objData = entry as Record<string, unknown>;
42
+ const className = objData.className as string;
43
+ const parentPath = objData.parent as string;
44
+
45
+ if (!className || !parentPath) {
46
+ failureCount++;
47
+ results.push({ success: false, error: "Class name and parent are required" });
48
+ continue;
49
+ }
50
+
51
+ const [entrySuccess, entryResult] = pcall(() => createFn(objData));
52
+ if (!entrySuccess) {
53
+ failureCount++;
54
+ results.push({ success: false, className, parent: parentPath, error: tostring(entryResult) });
55
+ continue;
56
+ }
57
+
58
+ if ("instance" in entryResult) {
59
+ successCount++;
60
+ results.push({
61
+ success: true,
62
+ className: entryResult.className,
63
+ parent: entryResult.parentPath,
64
+ instancePath: getInstancePath(entryResult.instance),
65
+ name: entryResult.instance.Name,
66
+ });
67
+ } else {
68
+ failureCount++;
69
+ results.push({
70
+ success: false,
71
+ className: entryResult.className ?? className,
72
+ parent: entryResult.parentPath ?? parentPath,
73
+ error: entryResult.error,
74
+ });
75
+ }
76
+ }
77
+ });
78
+
79
+ if (!loopSuccess) {
80
+ failureCount++;
81
+ results.push({ success: false, error: `Unexpected mass create failure: ${tostring(loopError)}` });
82
+ }
83
+
84
+ return { results, successCount, failureCount };
85
+ }
86
+
7
87
  function createObject(requestData: Record<string, unknown>) {
8
88
  const className = requestData.className as string;
9
89
  const parentPath = requestData.parent as string;
@@ -77,46 +157,30 @@ function massCreateObjects(requestData: Record<string, unknown>) {
77
157
  return { error: "Objects array is required" };
78
158
  }
79
159
 
80
- const results: Record<string, unknown>[] = [];
81
- let successCount = 0;
82
- let failureCount = 0;
83
160
  const recordingId = beginRecording("Mass create objects");
84
161
 
85
- for (const objData of objects) {
162
+ const { results, successCount, failureCount } = processObjectEntries(objects, (objData) => {
86
163
  const className = objData.className as string;
87
164
  const parentPath = objData.parent as string;
88
165
  const name = objData.name as string | undefined;
166
+ const parentInstance = getInstanceByPath(parentPath);
167
+ if (!parentInstance) {
168
+ return { error: "Parent instance not found", className, parentPath };
169
+ }
89
170
 
90
- if (className && parentPath) {
91
- const parentInstance = getInstanceByPath(parentPath);
92
- if (parentInstance) {
93
- const [success, newInstance] = pcall(() => {
94
- const instance = new Instance(className as keyof CreatableInstances);
95
- if (name) instance.Name = name;
96
- instance.Parent = parentInstance;
97
- return instance;
98
- });
171
+ const [success, newInstance] = pcall(() => {
172
+ const instance = new Instance(className as keyof CreatableInstances);
173
+ if (name) instance.Name = name;
174
+ instance.Parent = parentInstance;
175
+ return instance;
176
+ });
99
177
 
100
- if (success && newInstance) {
101
- successCount++;
102
- results.push({
103
- success: true, className, parent: parentPath,
104
- instancePath: getInstancePath(newInstance as Instance),
105
- name: (newInstance as Instance).Name,
106
- });
107
- } else {
108
- failureCount++;
109
- results.push({ success: false, className, parent: parentPath, error: tostring(newInstance) });
110
- }
111
- } else {
112
- failureCount++;
113
- results.push({ success: false, className, parent: parentPath, error: "Parent instance not found" });
114
- }
115
- } else {
116
- failureCount++;
117
- results.push({ success: false, error: "Class name and parent are required" });
178
+ if (!success || !newInstance) {
179
+ return { error: tostring(newInstance), className, parentPath };
118
180
  }
119
- }
181
+
182
+ return { instance: newInstance as Instance, className, parentPath };
183
+ });
120
184
 
121
185
  finishRecording(recordingId, successCount > 0);
122
186
  return { results, summary: { total: (objects as defined[]).size(), succeeded: successCount, failed: failureCount } };
@@ -128,56 +192,45 @@ function massCreateObjectsWithProperties(requestData: Record<string, unknown>) {
128
192
  return { error: "Objects array is required" };
129
193
  }
130
194
 
131
- const results: Record<string, unknown>[] = [];
132
- let successCount = 0;
133
- let failureCount = 0;
134
195
  const recordingId = beginRecording("Mass create objects with properties");
135
196
 
136
- for (const objData of objects) {
197
+ const { results, successCount, failureCount } = processObjectEntries(objects, (objData) => {
137
198
  const className = objData.className as string;
138
199
  const parentPath = objData.parent as string;
139
200
  const name = objData.name as string | undefined;
140
- const properties = (objData.properties as Record<string, unknown>) ?? {};
141
-
142
- if (className && parentPath) {
143
- const parentInstance = getInstanceByPath(parentPath);
144
- if (parentInstance) {
145
- const [success, newInstance] = pcall(() => {
146
- const instance = new Instance(className as keyof CreatableInstances);
147
- if (name) instance.Name = name;
148
- instance.Parent = parentInstance;
201
+ const propertiesRaw = objData.properties;
202
+ const properties = typeIs(propertiesRaw, "table")
203
+ ? (propertiesRaw as Record<string, unknown>)
204
+ : ({} as Record<string, unknown>);
205
+
206
+ const parentInstance = getInstanceByPath(parentPath);
207
+ if (!parentInstance) {
208
+ return { error: "Parent instance not found", className, parentPath };
209
+ }
149
210
 
150
- for (const [propName, propValue] of pairs(properties)) {
151
- pcall(() => {
152
- const converted = convertPropertyValue(instance, propName as string, propValue);
153
- if (converted !== undefined) {
154
- (instance as unknown as { [key: string]: unknown })[propName as string] = converted;
155
- }
156
- });
211
+ const [success, newInstance] = pcall(() => {
212
+ const instance = new Instance(className as keyof CreatableInstances);
213
+ if (name) instance.Name = name;
214
+ instance.Parent = parentInstance;
215
+
216
+ for (const [propName, propValue] of pairs(properties)) {
217
+ pcall(() => {
218
+ const propNameStr = tostring(propName);
219
+ const converted = convertPropertyValue(instance, propNameStr, propValue);
220
+ if (converted !== undefined) {
221
+ (instance as unknown as { [key: string]: unknown })[propNameStr] = converted;
157
222
  }
158
- return instance;
159
223
  });
160
-
161
- if (success && newInstance) {
162
- successCount++;
163
- results.push({
164
- success: true, className, parent: parentPath,
165
- instancePath: getInstancePath(newInstance as Instance),
166
- name: (newInstance as Instance).Name,
167
- });
168
- } else {
169
- failureCount++;
170
- results.push({ success: false, className, parent: parentPath, error: tostring(newInstance) });
171
- }
172
- } else {
173
- failureCount++;
174
- results.push({ success: false, className, parent: parentPath, error: "Parent instance not found" });
175
224
  }
176
- } else {
177
- failureCount++;
178
- results.push({ success: false, error: "Class name and parent are required" });
225
+ return instance;
226
+ });
227
+
228
+ if (!success || !newInstance) {
229
+ return { error: tostring(newInstance), className, parentPath };
179
230
  }
180
- }
231
+
232
+ return { instance: newInstance as Instance, className, parentPath };
233
+ });
181
234
 
182
235
  finishRecording(recordingId, successCount > 0);
183
236
  return { results, summary: { total: (objects as defined[]).size(), succeeded: successCount, failed: failureCount } };