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.
- package/dist/index.js +3507 -1061
- package/package.json +6 -31
- package/studio-plugin/MCPPlugin.rbxmx +4 -33
- package/studio-plugin/{ts-src → src}/modules/Communication.ts +14 -1
- package/studio-plugin/src/modules/Recording.ts +28 -0
- package/studio-plugin/{ts-src → src}/modules/UI.ts +2 -32
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +230 -0
- package/studio-plugin/src/modules/handlers/BuildHandlers.ts +476 -0
- package/studio-plugin/{ts-src → src}/modules/handlers/InstanceHandlers.ts +22 -16
- package/studio-plugin/{ts-src → src}/modules/handlers/MetadataHandlers.ts +54 -8
- package/studio-plugin/{ts-src → src}/modules/handlers/PropertyHandlers.ts +11 -12
- package/studio-plugin/{ts-src → src}/modules/handlers/QueryHandlers.ts +144 -1
- package/studio-plugin/{ts-src → src}/modules/handlers/ScriptHandlers.ts +38 -13
- package/studio-plugin/tsconfig.json +1 -1
- package/LICENSE +0 -21
- package/README.md +0 -71
- package/dist/__tests__/bridge-service.test.d.ts +0 -2
- package/dist/__tests__/bridge-service.test.d.ts.map +0 -1
- package/dist/__tests__/bridge-service.test.js +0 -95
- package/dist/__tests__/bridge-service.test.js.map +0 -1
- package/dist/__tests__/http-server.test.d.ts +0 -2
- package/dist/__tests__/http-server.test.d.ts.map +0 -1
- package/dist/__tests__/http-server.test.js +0 -176
- package/dist/__tests__/http-server.test.js.map +0 -1
- package/dist/__tests__/integration.test.d.ts +0 -2
- package/dist/__tests__/integration.test.d.ts.map +0 -1
- package/dist/__tests__/integration.test.js +0 -156
- package/dist/__tests__/integration.test.js.map +0 -1
- package/dist/__tests__/smoke.test.d.ts +0 -2
- package/dist/__tests__/smoke.test.d.ts.map +0 -1
- package/dist/__tests__/smoke.test.js +0 -53
- package/dist/__tests__/smoke.test.js.map +0 -1
- package/dist/bridge-service.d.ts +0 -17
- package/dist/bridge-service.d.ts.map +0 -1
- package/dist/bridge-service.js +0 -78
- package/dist/bridge-service.js.map +0 -1
- package/dist/http-server.d.ts +0 -4
- package/dist/http-server.d.ts.map +0 -1
- package/dist/http-server.js +0 -478
- package/dist/http-server.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tools/index.d.ts +0 -273
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -587
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/studio-client.d.ts +0 -7
- package/dist/tools/studio-client.d.ts.map +0 -1
- package/dist/tools/studio-client.js +0 -19
- package/dist/tools/studio-client.js.map +0 -1
- /package/studio-plugin/{ts-src → src}/modules/State.ts +0 -0
- /package/studio-plugin/{ts-src → src}/modules/Utils.ts +0 -0
- /package/studio-plugin/{ts-src → src}/modules/handlers/TestHandlers.ts +0 -0
- /package/studio-plugin/{ts-src → src}/server/index.server.ts +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
304
|
-
ChangeHistoryService.SetWaypoint(`Mass duplicate operations (${totalSuccess} objects)`);
|
|
305
|
-
}
|
|
311
|
+
finishRecording(recordingId, totalSuccess > 0);
|
|
306
312
|
|
|
307
313
|
return {
|
|
308
314
|
results: allResults,
|