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.
- package/dist/index.js +173 -10
- package/dist/install-plugin-cli.js +87 -0
- package/package.json +3 -2
- package/studio-plugin/MCPPlugin.rbxmx +1522 -183
- package/studio-plugin/src/modules/Communication.ts +3 -0
- package/studio-plugin/src/modules/Utils.ts +1 -1
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +79 -68
- package/studio-plugin/src/modules/handlers/BuildHandlers.ts +36 -31
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +118 -0
- package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +125 -72
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +25 -8
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +1 -0
|
@@ -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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
37
|
-
|
|
32
|
+
const insertedInstances: Instance[] = [];
|
|
33
|
+
const children = loadedWrapper.GetChildren();
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
for (const child of children) {
|
|
36
|
+
child.Parent = parentInstance;
|
|
37
|
+
insertedInstances.push(child);
|
|
38
|
+
}
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
inst.
|
|
50
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
77
|
+
if (wrapperModel) {
|
|
78
|
+
pcall(() => {
|
|
79
|
+
wrapperModel!.Destroy();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
69
82
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
finishRecording(recordingId, insertSuccess);
|
|
84
|
+
|
|
85
|
+
if (!insertSuccess) {
|
|
86
|
+
return { error: `Failed to insert asset ${assetId}: ${tostring(insertResult)}` };
|
|
87
|
+
}
|
|
74
88
|
|
|
75
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 } };
|