robloxstudio-mcp 2.2.8 → 2.4.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.
Files changed (56) hide show
  1. package/dist/index.js +3507 -1061
  2. package/package.json +6 -31
  3. package/studio-plugin/MCPPlugin.rbxmx +4 -33
  4. package/studio-plugin/{ts-src → src}/modules/Communication.ts +14 -1
  5. package/studio-plugin/src/modules/Recording.ts +28 -0
  6. package/studio-plugin/{ts-src → src}/modules/UI.ts +2 -32
  7. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +230 -0
  8. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +476 -0
  9. package/studio-plugin/{ts-src → src}/modules/handlers/InstanceHandlers.ts +22 -16
  10. package/studio-plugin/{ts-src → src}/modules/handlers/MetadataHandlers.ts +54 -8
  11. package/studio-plugin/{ts-src → src}/modules/handlers/PropertyHandlers.ts +11 -12
  12. package/studio-plugin/{ts-src → src}/modules/handlers/QueryHandlers.ts +144 -1
  13. package/studio-plugin/{ts-src → src}/modules/handlers/ScriptHandlers.ts +38 -13
  14. package/studio-plugin/tsconfig.json +1 -1
  15. package/LICENSE +0 -21
  16. package/README.md +0 -71
  17. package/dist/__tests__/bridge-service.test.d.ts +0 -2
  18. package/dist/__tests__/bridge-service.test.d.ts.map +0 -1
  19. package/dist/__tests__/bridge-service.test.js +0 -95
  20. package/dist/__tests__/bridge-service.test.js.map +0 -1
  21. package/dist/__tests__/http-server.test.d.ts +0 -2
  22. package/dist/__tests__/http-server.test.d.ts.map +0 -1
  23. package/dist/__tests__/http-server.test.js +0 -176
  24. package/dist/__tests__/http-server.test.js.map +0 -1
  25. package/dist/__tests__/integration.test.d.ts +0 -2
  26. package/dist/__tests__/integration.test.d.ts.map +0 -1
  27. package/dist/__tests__/integration.test.js +0 -156
  28. package/dist/__tests__/integration.test.js.map +0 -1
  29. package/dist/__tests__/smoke.test.d.ts +0 -2
  30. package/dist/__tests__/smoke.test.d.ts.map +0 -1
  31. package/dist/__tests__/smoke.test.js +0 -53
  32. package/dist/__tests__/smoke.test.js.map +0 -1
  33. package/dist/bridge-service.d.ts +0 -17
  34. package/dist/bridge-service.d.ts.map +0 -1
  35. package/dist/bridge-service.js +0 -78
  36. package/dist/bridge-service.js.map +0 -1
  37. package/dist/http-server.d.ts +0 -4
  38. package/dist/http-server.d.ts.map +0 -1
  39. package/dist/http-server.js +0 -478
  40. package/dist/http-server.js.map +0 -1
  41. package/dist/index.d.ts +0 -3
  42. package/dist/index.d.ts.map +0 -1
  43. package/dist/index.js.map +0 -1
  44. package/dist/tools/index.d.ts +0 -273
  45. package/dist/tools/index.d.ts.map +0 -1
  46. package/dist/tools/index.js +0 -587
  47. package/dist/tools/index.js.map +0 -1
  48. package/dist/tools/studio-client.d.ts +0 -7
  49. package/dist/tools/studio-client.d.ts.map +0 -1
  50. package/dist/tools/studio-client.js +0 -19
  51. package/dist/tools/studio-client.js.map +0 -1
  52. /package/studio-plugin/{ts-src → src}/modules/State.ts +0 -0
  53. /package/studio-plugin/{ts-src → src}/modules/Utils.ts +0 -0
  54. /package/studio-plugin/{ts-src → src}/modules/handlers/TestHandlers.ts +0 -0
  55. /package/studio-plugin/{ts-src → src}/server/index.server.ts +0 -0
  56. /package/studio-plugin/{ts-src → src}/types/index.d.ts +0 -0
@@ -0,0 +1,476 @@
1
+ import Utils from "../Utils";
2
+ import Recording from "../Recording";
3
+
4
+ const MaterialService = game.GetService("MaterialService");
5
+
6
+ const { getInstancePath, getInstanceByPath } = Utils;
7
+ const { beginRecording, finishRecording } = Recording;
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
+ };
33
+
34
+ // Shape class mapping
35
+ const SHAPE_CLASSES: Record<string, string> = {
36
+ Block: "Part",
37
+ Wedge: "WedgePart",
38
+ Cylinder: "Part",
39
+ Ball: "Part",
40
+ CornerWedge: "CornerWedgePart",
41
+ };
42
+
43
+ const PALETTE_KEYS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
44
+
45
+ function roundTo(n: number, decimals: number): number {
46
+ const mult = 10 ** decimals;
47
+ return math.round(n * mult) / mult;
48
+ }
49
+
50
+ function exportBuild(requestData: Record<string, unknown>) {
51
+ const instancePath = requestData.instancePath as string;
52
+ const outputId = requestData.outputId as string;
53
+ const style = (requestData.style as string) ?? "misc";
54
+
55
+ if (!instancePath) {
56
+ return { error: "Instance path is required" };
57
+ }
58
+
59
+ const instance = getInstanceByPath(instancePath);
60
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
61
+
62
+ if (!instance.IsA("Model") && !instance.IsA("Folder")) {
63
+ return { error: "Instance must be a Model or Folder" };
64
+ }
65
+
66
+ const [success, result] = pcall(() => {
67
+ const descendants = instance.GetDescendants();
68
+ const baseParts: BasePart[] = [];
69
+ for (const desc of descendants) {
70
+ if (desc.IsA("BasePart")) {
71
+ baseParts.push(desc);
72
+ }
73
+ }
74
+
75
+ if (baseParts.size() === 0) {
76
+ return { error: "No BaseParts found in instance" };
77
+ }
78
+
79
+ // Compute bounding box center
80
+ let minX = math.huge;
81
+ let minY = math.huge;
82
+ let minZ = math.huge;
83
+ let maxX = -math.huge;
84
+ let maxY = -math.huge;
85
+ let maxZ = -math.huge;
86
+
87
+ for (const part of baseParts) {
88
+ const pos = part.Position;
89
+ const sz = part.Size;
90
+ const halfX = sz.X / 2;
91
+ const halfY = sz.Y / 2;
92
+ const halfZ = sz.Z / 2;
93
+ minX = math.min(minX, pos.X - halfX);
94
+ minY = math.min(minY, pos.Y - halfY);
95
+ minZ = math.min(minZ, pos.Z - halfZ);
96
+ maxX = math.max(maxX, pos.X + halfX);
97
+ maxY = math.max(maxY, pos.Y + halfY);
98
+ maxZ = math.max(maxZ, pos.Z + halfZ);
99
+ }
100
+
101
+ const centerX = (minX + maxX) / 2;
102
+ const centerY = minY; // Use bottom as Y origin
103
+ const centerZ = (minZ + maxZ) / 2;
104
+ const boundsX = roundTo(maxX - minX, 1);
105
+ const boundsY = roundTo(maxY - minY, 1);
106
+ const boundsZ = roundTo(maxZ - minZ, 1);
107
+
108
+ // Build palette from unique (BrickColor, Material, MaterialVariant?) combos
109
+ const paletteMap = new Map<string, string>();
110
+ const palette: Record<string, [string, string] | [string, string, string]> = {};
111
+ let keyIndex = 0;
112
+
113
+ for (const part of baseParts) {
114
+ const colorName = part.BrickColor.Name;
115
+ const materialName = part.Material.Name;
116
+ const variantName = part.MaterialVariant;
117
+ const combo = variantName !== "" ? `${colorName}|${materialName}|${variantName}` : `${colorName}|${materialName}`;
118
+
119
+ if (!paletteMap.has(combo)) {
120
+ const key = PALETTE_KEYS.sub(keyIndex + 1, keyIndex + 1);
121
+ keyIndex++;
122
+ paletteMap.set(combo, key);
123
+ if (variantName !== "") {
124
+ palette[key] = [colorName, materialName, variantName];
125
+ } else {
126
+ palette[key] = [colorName, materialName];
127
+ }
128
+ }
129
+ }
130
+
131
+ // Build compact part arrays
132
+ const parts: unknown[][] = [];
133
+ for (const part of baseParts) {
134
+ const pos = part.Position;
135
+ const orient = part.Orientation;
136
+ const sz = part.Size;
137
+ const colorName = part.BrickColor.Name;
138
+ const materialName = part.Material.Name;
139
+ const combo = `${colorName}|${materialName}`;
140
+ const paletteKey = paletteMap.get(combo) ?? "a";
141
+
142
+ // Relative position to center
143
+ const relX = roundTo(pos.X - centerX, 1);
144
+ const relY = roundTo(pos.Y - centerY, 1);
145
+ const relZ = roundTo(pos.Z - centerZ, 1);
146
+ const sizeX = roundTo(sz.X, 1);
147
+ const sizeY = roundTo(sz.Y, 1);
148
+ const sizeZ = roundTo(sz.Z, 1);
149
+ const rotX = roundTo(orient.X, 1);
150
+ const rotY = roundTo(orient.Y, 1);
151
+ const rotZ = roundTo(orient.Z, 1);
152
+
153
+ // Determine shape
154
+ let shape = "Block";
155
+ if (part.IsA("WedgePart")) {
156
+ shape = "Wedge";
157
+ } else if (part.IsA("CornerWedgePart")) {
158
+ shape = "CornerWedge";
159
+ } else if (part.IsA("Part")) {
160
+ const p = part as Part;
161
+ if (p.Shape === Enum.PartType.Cylinder) {
162
+ shape = "Cylinder";
163
+ } else if (p.Shape === Enum.PartType.Ball) {
164
+ shape = "Ball";
165
+ }
166
+ }
167
+
168
+ // Build part array with optional trailing fields
169
+ const hasTransparency = part.Transparency > 0;
170
+ const hasShape = shape !== "Block";
171
+
172
+ let partArr: defined[];
173
+ if (hasTransparency) {
174
+ partArr = [relX, relY, relZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey, hasShape ? shape : "Block", roundTo(part.Transparency, 2)];
175
+ } else if (hasShape) {
176
+ partArr = [relX, relY, relZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey, shape];
177
+ } else {
178
+ partArr = [relX, relY, relZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey];
179
+ }
180
+
181
+ parts.push(partArr);
182
+ }
183
+
184
+ const buildId = outputId ?? `${style}/${instance.Name.lower().gsub(" ", "_")[0]}`;
185
+
186
+ return {
187
+ success: true,
188
+ buildData: {
189
+ id: buildId,
190
+ style: style,
191
+ bounds: [boundsX, boundsY, boundsZ],
192
+ palette: palette,
193
+ parts: parts,
194
+ },
195
+ };
196
+ });
197
+
198
+ if (success && result) {
199
+ return result;
200
+ } else {
201
+ return { error: `Failed to export build: ${result}` };
202
+ }
203
+ }
204
+
205
+ function importBuild(requestData: Record<string, unknown>) {
206
+ const buildData = requestData.buildData as Record<string, unknown>;
207
+ const targetPath = requestData.targetPath as string;
208
+ const positionOffset = (requestData.position as number[]) ?? [0, 0, 0];
209
+
210
+ if (!buildData || !targetPath) {
211
+ return { error: "buildData and targetPath are required" };
212
+ }
213
+
214
+ const parentInstance = getInstanceByPath(targetPath);
215
+ if (!parentInstance) return { error: `Target not found: ${targetPath}` };
216
+ const recordingId = beginRecording("Import build");
217
+
218
+ const [success, result] = pcall(() => {
219
+ const palette = buildData.palette as Record<string, [string, string, string?]>;
220
+ const parts = buildData.parts as unknown[][];
221
+ const buildId = (buildData.id as string) ?? "imported_build";
222
+
223
+ // Create model container
224
+ const model = new Instance("Model");
225
+ model.Name = buildId.match("[^/]+$")[0] as string ?? buildId;
226
+
227
+ let partCount = 0;
228
+
229
+ for (const partArr of parts) {
230
+ const posX = (partArr[0] as number) + (positionOffset[0] ?? 0);
231
+ const posY = (partArr[1] as number) + (positionOffset[1] ?? 0);
232
+ const posZ = (partArr[2] as number) + (positionOffset[2] ?? 0);
233
+ const sizeX = partArr[3] as number;
234
+ const sizeY = partArr[4] as number;
235
+ const sizeZ = partArr[5] as number;
236
+ const rotX = partArr[6] as number;
237
+ const rotY = partArr[7] as number;
238
+ const rotZ = partArr[8] as number;
239
+ const paletteKey = partArr[9] as string;
240
+ const shape = (partArr[10] as string) ?? "Block";
241
+ const transparency = (partArr[11] as number) ?? 0;
242
+
243
+ // Determine class from shape
244
+ const className = SHAPE_CLASSES[shape] ?? "Part";
245
+ const part = new Instance(className as keyof CreatableInstances) as BasePart;
246
+
247
+ // Set shape for Part instances with non-Block shapes
248
+ if (className === "Part" && shape !== "Block") {
249
+ if (shape === "Cylinder") {
250
+ (part as Part).Shape = Enum.PartType.Cylinder;
251
+ } else if (shape === "Ball") {
252
+ (part as Part).Shape = Enum.PartType.Ball;
253
+ }
254
+ }
255
+
256
+ part.Size = new Vector3(sizeX, sizeY, sizeZ);
257
+ part.Position = new Vector3(posX, posY, posZ);
258
+ part.Orientation = new Vector3(rotX, rotY, rotZ);
259
+ part.Anchored = true;
260
+
261
+ if (transparency > 0) {
262
+ part.Transparency = transparency;
263
+ }
264
+
265
+ // Apply palette
266
+ const paletteEntry = palette[paletteKey];
267
+ if (paletteEntry) {
268
+ const [colorName, materialName, variantName] = paletteEntry;
269
+ pcall(() => {
270
+ part.BrickColor = new BrickColor(colorName as unknown as number);
271
+ });
272
+ pcall(() => {
273
+ const mat = MATERIAL_NAMES[materialName];
274
+ if (mat) {
275
+ part.Material = mat;
276
+ }
277
+ });
278
+ // Apply MaterialVariant if specified
279
+ if (variantName !== undefined && variantName !== "") {
280
+ pcall(() => {
281
+ part.MaterialVariant = variantName;
282
+ });
283
+ }
284
+ }
285
+
286
+ part.Parent = model;
287
+ partCount++;
288
+ }
289
+
290
+ model.Parent = parentInstance;
291
+
292
+ return {
293
+ success: true,
294
+ partCount: partCount,
295
+ modelPath: getInstancePath(model),
296
+ };
297
+ });
298
+
299
+ if (success && result) {
300
+ finishRecording(recordingId, true);
301
+ return result;
302
+ } else {
303
+ finishRecording(recordingId, false);
304
+ return { error: `Failed to import build: ${result}` };
305
+ }
306
+ }
307
+
308
+ function importScene(requestData: Record<string, unknown>) {
309
+ const expandedBuilds = requestData.expandedBuilds as Record<string, unknown>[];
310
+ const targetPath = (requestData.targetPath as string) ?? "game.Workspace";
311
+
312
+ if (!expandedBuilds || !typeIs(expandedBuilds, "table") || (expandedBuilds as defined[]).size() === 0) {
313
+ return { error: "expandedBuilds array is required" };
314
+ }
315
+
316
+ const parentInstance = getInstanceByPath(targetPath);
317
+ if (!parentInstance) return { error: `Target not found: ${targetPath}` };
318
+ const recordingId = beginRecording("Import scene");
319
+
320
+ const [success, result] = pcall(() => {
321
+ let modelCount = 0;
322
+ let totalParts = 0;
323
+ const models: Record<string, unknown>[] = [];
324
+
325
+ for (const entry of expandedBuilds) {
326
+ const buildData = entry.buildData as Record<string, unknown>;
327
+ const position = (entry.position as number[]) ?? [0, 0, 0];
328
+ const rotation = (entry.rotation as number[]) ?? [0, 0, 0];
329
+ const name = (entry.name as string) ?? "SceneModel";
330
+
331
+ const palette = buildData.palette as Record<string, [string, string, string?]>;
332
+ const parts = buildData.parts as unknown[][];
333
+
334
+ const model = new Instance("Model");
335
+ model.Name = name;
336
+
337
+ const rotCF = CFrame.Angles(
338
+ math.rad(rotation[0] ?? 0),
339
+ math.rad(rotation[1] ?? 0),
340
+ math.rad(rotation[2] ?? 0),
341
+ );
342
+ const originCF = new CFrame(position[0] ?? 0, position[1] ?? 0, position[2] ?? 0).mul(rotCF);
343
+
344
+ let partCount = 0;
345
+
346
+ for (const partArr of parts) {
347
+ const localX = partArr[0] as number;
348
+ const localY = partArr[1] as number;
349
+ const localZ = partArr[2] as number;
350
+ const sizeX = partArr[3] as number;
351
+ const sizeY = partArr[4] as number;
352
+ const sizeZ = partArr[5] as number;
353
+ const rotX = partArr[6] as number;
354
+ const rotY = partArr[7] as number;
355
+ const rotZ = partArr[8] as number;
356
+ const paletteKey = partArr[9] as string;
357
+ const shape = (partArr[10] as string) ?? "Block";
358
+ const transparency = (partArr[11] as number) ?? 0;
359
+
360
+ const className = SHAPE_CLASSES[shape] ?? "Part";
361
+ const part = new Instance(className as keyof CreatableInstances) as BasePart;
362
+
363
+ if (className === "Part" && shape !== "Block") {
364
+ if (shape === "Cylinder") {
365
+ (part as Part).Shape = Enum.PartType.Cylinder;
366
+ } else if (shape === "Ball") {
367
+ (part as Part).Shape = Enum.PartType.Ball;
368
+ }
369
+ }
370
+
371
+ part.Size = new Vector3(sizeX, sizeY, sizeZ);
372
+
373
+ // Apply local rotation then world transform
374
+ const localRotCF = CFrame.Angles(math.rad(rotX), math.rad(rotY), math.rad(rotZ));
375
+ const localPosCF = new CFrame(localX, localY, localZ).mul(localRotCF);
376
+ const worldCF = originCF.mul(localPosCF);
377
+
378
+ part.CFrame = worldCF;
379
+ part.Anchored = true;
380
+
381
+ if (transparency > 0) {
382
+ part.Transparency = transparency;
383
+ }
384
+
385
+ const paletteEntry = palette[paletteKey];
386
+ if (paletteEntry) {
387
+ const [colorName, materialName, variantName] = paletteEntry;
388
+ pcall(() => {
389
+ part.BrickColor = new BrickColor(colorName as unknown as number);
390
+ });
391
+ pcall(() => {
392
+ const mat = MATERIAL_NAMES[materialName];
393
+ if (mat) {
394
+ part.Material = mat;
395
+ }
396
+ });
397
+ if (variantName !== undefined && variantName !== "") {
398
+ pcall(() => {
399
+ part.MaterialVariant = variantName;
400
+ });
401
+ }
402
+ }
403
+
404
+ part.Parent = model;
405
+ partCount++;
406
+ }
407
+
408
+ model.Parent = parentInstance;
409
+ modelCount++;
410
+ totalParts += partCount;
411
+ models.push({
412
+ name: name,
413
+ partCount: partCount,
414
+ modelPath: getInstancePath(model),
415
+ });
416
+ }
417
+
418
+ return {
419
+ success: true,
420
+ modelCount: modelCount,
421
+ totalParts: totalParts,
422
+ models: models,
423
+ };
424
+ });
425
+
426
+ if (success && result) {
427
+ finishRecording(recordingId, true);
428
+ return result;
429
+ } else {
430
+ finishRecording(recordingId, false);
431
+ return { error: `Failed to import scene: ${result}` };
432
+ }
433
+ }
434
+
435
+ function searchMaterials(requestData: Record<string, unknown>) {
436
+ const query = ((requestData.query as string) ?? "").lower();
437
+ const maxResults = (requestData.maxResults as number) ?? 50;
438
+
439
+ const [success, result] = pcall(() => {
440
+ const children = MaterialService.GetChildren();
441
+ const materials: Record<string, unknown>[] = [];
442
+
443
+ for (const child of children) {
444
+ if (!child.IsA("MaterialVariant")) continue;
445
+
446
+ const nameMatch = query === "" || child.Name.lower().find(query)[0] !== undefined;
447
+ if (!nameMatch) continue;
448
+
449
+ materials.push({
450
+ name: child.Name,
451
+ baseMaterial: child.BaseMaterial.Name,
452
+ });
453
+
454
+ if (materials.size() >= maxResults) break;
455
+ }
456
+
457
+ return {
458
+ success: true,
459
+ materials: materials,
460
+ total: materials.size(),
461
+ };
462
+ });
463
+
464
+ if (success && result) {
465
+ return result;
466
+ } else {
467
+ return { error: `Failed to search materials: ${result}` };
468
+ }
469
+ }
470
+
471
+ export = {
472
+ exportBuild,
473
+ importBuild,
474
+ importScene,
475
+ searchMaterials,
476
+ };
@@ -1,8 +1,8 @@
1
1
  import Utils from "../Utils";
2
-
3
- const ChangeHistoryService = game.GetService("ChangeHistoryService");
2
+ import Recording from "../Recording";
4
3
 
5
4
  const { getInstancePath, getInstanceByPath, convertPropertyValue } = Utils;
5
+ const { beginRecording, finishRecording } = Recording;
6
6
 
7
7
  function createObject(requestData: Record<string, unknown>) {
8
8
  const className = requestData.className as string;
@@ -16,6 +16,7 @@ function createObject(requestData: Record<string, unknown>) {
16
16
 
17
17
  const parentInstance = getInstanceByPath(parentPath);
18
18
  if (!parentInstance) return { error: `Parent instance not found: ${parentPath}` };
19
+ const recordingId = beginRecording(`Create ${className}`);
19
20
 
20
21
  const [success, newInstance] = pcall(() => {
21
22
  const instance = new Instance(className as keyof CreatableInstances);
@@ -28,11 +29,11 @@ function createObject(requestData: Record<string, unknown>) {
28
29
  }
29
30
 
30
31
  instance.Parent = parentInstance;
31
- ChangeHistoryService.SetWaypoint(`Create ${className}`);
32
32
  return instance;
33
33
  });
34
34
 
35
35
  if (success && newInstance) {
36
+ finishRecording(recordingId, true);
36
37
  return {
37
38
  success: true,
38
39
  className,
@@ -42,6 +43,7 @@ function createObject(requestData: Record<string, unknown>) {
42
43
  message: "Object created successfully",
43
44
  };
44
45
  } else {
46
+ finishRecording(recordingId, false);
45
47
  return { error: `Failed to create object: ${newInstance}`, className, parent: parentPath };
46
48
  }
47
49
  }
@@ -53,18 +55,18 @@ function deleteObject(requestData: Record<string, unknown>) {
53
55
  const instance = getInstanceByPath(instancePath);
54
56
  if (!instance) return { error: `Instance not found: ${instancePath}` };
55
57
  if (instance === game) return { error: "Cannot delete the game instance" };
58
+ const recordingId = beginRecording(`Delete ${instance.ClassName} (${instance.Name})`);
56
59
 
57
60
  const [success, result] = pcall(() => {
58
- const name = instance.Name;
59
- const className = instance.ClassName;
60
61
  instance.Destroy();
61
- ChangeHistoryService.SetWaypoint(`Delete ${className} (${name})`);
62
62
  return true;
63
63
  });
64
64
 
65
65
  if (success) {
66
+ finishRecording(recordingId, true);
66
67
  return { success: true, instancePath, message: "Object deleted successfully" };
67
68
  } else {
69
+ finishRecording(recordingId, false);
68
70
  return { error: `Failed to delete object: ${result}`, instancePath };
69
71
  }
70
72
  }
@@ -78,6 +80,7 @@ function massCreateObjects(requestData: Record<string, unknown>) {
78
80
  const results: Record<string, unknown>[] = [];
79
81
  let successCount = 0;
80
82
  let failureCount = 0;
83
+ const recordingId = beginRecording("Mass create objects");
81
84
 
82
85
  for (const objData of objects) {
83
86
  const className = objData.className as string;
@@ -115,7 +118,7 @@ function massCreateObjects(requestData: Record<string, unknown>) {
115
118
  }
116
119
  }
117
120
 
118
- if (successCount > 0) ChangeHistoryService.SetWaypoint("Mass create objects");
121
+ finishRecording(recordingId, successCount > 0);
119
122
  return { results, summary: { total: (objects as defined[]).size(), succeeded: successCount, failed: failureCount } };
120
123
  }
121
124
 
@@ -128,6 +131,7 @@ function massCreateObjectsWithProperties(requestData: Record<string, unknown>) {
128
131
  const results: Record<string, unknown>[] = [];
129
132
  let successCount = 0;
130
133
  let failureCount = 0;
134
+ const recordingId = beginRecording("Mass create objects with properties");
131
135
 
132
136
  for (const objData of objects) {
133
137
  const className = objData.className as string;
@@ -175,11 +179,11 @@ function massCreateObjectsWithProperties(requestData: Record<string, unknown>) {
175
179
  }
176
180
  }
177
181
 
178
- if (successCount > 0) ChangeHistoryService.SetWaypoint("Mass create objects with properties");
182
+ finishRecording(recordingId, successCount > 0);
179
183
  return { results, summary: { total: (objects as defined[]).size(), succeeded: successCount, failed: failureCount } };
180
184
  }
181
185
 
182
- function smartDuplicate(requestData: Record<string, unknown>) {
186
+ function performSmartDuplicate(requestData: Record<string, unknown>, useRecording = true) {
183
187
  const instancePath = requestData.instancePath as string;
184
188
  const count = requestData.count as number;
185
189
  const options = (requestData.options as Record<string, unknown>) ?? {};
@@ -190,6 +194,7 @@ function smartDuplicate(requestData: Record<string, unknown>) {
190
194
 
191
195
  const instance = getInstanceByPath(instancePath);
192
196
  if (!instance) return { error: `Instance not found: ${instancePath}` };
197
+ const recordingId = useRecording ? beginRecording(`Smart duplicate ${instance.Name}`) : undefined;
193
198
 
194
199
  const results: Record<string, unknown>[] = [];
195
200
  let successCount = 0;
@@ -270,9 +275,7 @@ function smartDuplicate(requestData: Record<string, unknown>) {
270
275
  }
271
276
  }
272
277
 
273
- if (successCount > 0) {
274
- ChangeHistoryService.SetWaypoint(`Smart duplicate ${instance.Name} (${successCount} copies)`);
275
- }
278
+ finishRecording(recordingId, successCount > 0);
276
279
 
277
280
  return {
278
281
  results,
@@ -281,6 +284,10 @@ function smartDuplicate(requestData: Record<string, unknown>) {
281
284
  };
282
285
  }
283
286
 
287
+ function smartDuplicate(requestData: Record<string, unknown>) {
288
+ return performSmartDuplicate(requestData, true);
289
+ }
290
+
284
291
  function massDuplicate(requestData: Record<string, unknown>) {
285
292
  const duplications = requestData.duplications as Record<string, unknown>[];
286
293
  if (!duplications || !typeIs(duplications, "table") || (duplications as defined[]).size() === 0) {
@@ -290,9 +297,10 @@ function massDuplicate(requestData: Record<string, unknown>) {
290
297
  const allResults: Record<string, unknown>[] = [];
291
298
  let totalSuccess = 0;
292
299
  let totalFailures = 0;
300
+ const recordingId = beginRecording("Mass duplicate operations");
293
301
 
294
302
  for (const duplication of duplications) {
295
- const result = smartDuplicate(duplication) as { summary?: { succeeded: number; failed: number } };
303
+ const result = performSmartDuplicate(duplication, false) as { summary?: { succeeded: number; failed: number } };
296
304
  allResults.push(result as unknown as Record<string, unknown>);
297
305
  if (result.summary) {
298
306
  totalSuccess += result.summary.succeeded;
@@ -300,9 +308,7 @@ function massDuplicate(requestData: Record<string, unknown>) {
300
308
  }
301
309
  }
302
310
 
303
- if (totalSuccess > 0) {
304
- ChangeHistoryService.SetWaypoint(`Mass duplicate operations (${totalSuccess} objects)`);
305
- }
311
+ finishRecording(recordingId, totalSuccess > 0);
306
312
 
307
313
  return {
308
314
  results: allResults,