@princeofscale/bloxforge-inspector 2.20.2

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 (49) hide show
  1. package/assets/Baseplate.rbxl +0 -0
  2. package/dist/index.js +19281 -0
  3. package/package.json +62 -0
  4. package/studio-plugin/INSTALLATION.md +161 -0
  5. package/studio-plugin/MCPInspectorPlugin.rbxmx +171699 -0
  6. package/studio-plugin/MCPPlugin.rbxmx +171699 -0
  7. package/studio-plugin/default.project.json +19 -0
  8. package/studio-plugin/dev.project.json +23 -0
  9. package/studio-plugin/include/LibMP.lua +156378 -0
  10. package/studio-plugin/inspector-icon.png +0 -0
  11. package/studio-plugin/package-lock.json +692 -0
  12. package/studio-plugin/package.json +19 -0
  13. package/studio-plugin/plugin.json +10 -0
  14. package/studio-plugin/src/modules/ClientBroker.ts +447 -0
  15. package/studio-plugin/src/modules/Communication.ts +697 -0
  16. package/studio-plugin/src/modules/EvalBridges.ts +255 -0
  17. package/studio-plugin/src/modules/HttpDiagnostics.ts +50 -0
  18. package/studio-plugin/src/modules/JobRegistry.ts +77 -0
  19. package/studio-plugin/src/modules/LuauExec.ts +465 -0
  20. package/studio-plugin/src/modules/Recording.ts +28 -0
  21. package/studio-plugin/src/modules/RenderMonitor.ts +60 -0
  22. package/studio-plugin/src/modules/RuntimeLogBuffer.ts +152 -0
  23. package/studio-plugin/src/modules/ServerUrlSettings.ts +114 -0
  24. package/studio-plugin/src/modules/State.ts +101 -0
  25. package/studio-plugin/src/modules/StopPlayMonitor.ts +276 -0
  26. package/studio-plugin/src/modules/UI.ts +790 -0
  27. package/studio-plugin/src/modules/Utils.ts +318 -0
  28. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +241 -0
  29. package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +460 -0
  30. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +481 -0
  31. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +203 -0
  32. package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +149 -0
  33. package/studio-plugin/src/modules/handlers/InputHandlers.ts +160 -0
  34. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +380 -0
  35. package/studio-plugin/src/modules/handlers/JobHandlers.ts +111 -0
  36. package/studio-plugin/src/modules/handlers/LogHandlers.ts +14 -0
  37. package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +44 -0
  38. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +354 -0
  39. package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +1263 -0
  40. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +191 -0
  41. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +809 -0
  42. package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +216 -0
  43. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +530 -0
  44. package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +386 -0
  45. package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +172 -0
  46. package/studio-plugin/src/modules/handlers/TestHandlers.ts +535 -0
  47. package/studio-plugin/src/server/index.server.ts +134 -0
  48. package/studio-plugin/src/types/index.d.ts +83 -0
  49. package/studio-plugin/tsconfig.json +21 -0
@@ -0,0 +1,149 @@
1
+ import { LogService, ReplicatedStorage, RunService, ServerScriptService } from "@rbxts/services";
2
+ import { BRIDGE_NAMES, ensureRuntimeBridgeInstalled } from "../EvalBridges";
3
+ import LuauExec from "../LuauExec";
4
+
5
+ const PAYLOAD_INSTANCE_NAME = "__MCPEvalPayload";
6
+
7
+ interface BridgeInvokeResult {
8
+ ok?: boolean;
9
+ value?: unknown;
10
+ }
11
+
12
+ interface WrapperResult {
13
+ ok?: boolean;
14
+ value?: unknown;
15
+ output?: unknown;
16
+ }
17
+
18
+ function findBridge(config: { service: Instance; bridgeName: string }): BindableFunction | undefined {
19
+ const bridge = config.service.FindFirstChild(config.bridgeName);
20
+ return bridge && bridge.IsA("BindableFunction") ? bridge : undefined;
21
+ }
22
+
23
+ function waitForBridge(config: { service: Instance; bridgeName: string }, timeoutSec = 2): BindableFunction | undefined {
24
+ const deadline = tick() + timeoutSec;
25
+ let bridge = findBridge(config);
26
+ while (!bridge && tick() < deadline) {
27
+ task.wait(0.05);
28
+ bridge = findBridge(config);
29
+ }
30
+ return bridge;
31
+ }
32
+
33
+ function getBridgeConfig() {
34
+ if (!RunService.IsRunning()) {
35
+ return {
36
+ error: "eval_*_runtime requires a running playtest.",
37
+ };
38
+ }
39
+ if (RunService.IsServer()) {
40
+ return {
41
+ service: ServerScriptService,
42
+ bridgeName: BRIDGE_NAMES.serverLocal,
43
+ missingError: "ServerEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically in the runtime server peer, including for manually-started playtests.",
44
+ };
45
+ }
46
+ return {
47
+ service: ReplicatedStorage,
48
+ bridgeName: BRIDGE_NAMES.clientLocal,
49
+ missingError: "ClientEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically in the runtime client peer, including for manually-started playtests.",
50
+ };
51
+ }
52
+
53
+ function evalRuntime(requestData: Record<string, unknown>) {
54
+ const code = requestData.code as string;
55
+ if (!code || code === "") return { error: "Code is required" };
56
+
57
+ const config = getBridgeConfig();
58
+ if (config.error !== undefined) {
59
+ return { bridge: "missing", error: config.error };
60
+ }
61
+
62
+ let bridge = findBridge(config);
63
+ if (!bridge) {
64
+ const install = ensureRuntimeBridgeInstalled();
65
+ if (!install.installed) {
66
+ return {
67
+ bridge: "missing",
68
+ error: `${config.missingError} Runtime bridge install failed: ${install.error}`,
69
+ };
70
+ }
71
+ bridge = waitForBridge(config);
72
+ }
73
+ if (!bridge) {
74
+ return {
75
+ bridge: "missing",
76
+ error: `${config.missingError} Runtime bridge was installed but did not become ready.`,
77
+ };
78
+ }
79
+
80
+ const m = new Instance("ModuleScript");
81
+ m.Name = PAYLOAD_INSTANCE_NAME;
82
+ const userLines = LuauExec.countLines(code);
83
+ const wrapped = LuauExec.buildWrapper(code, PAYLOAD_INSTANCE_NAME);
84
+
85
+ const [okSet, setErr] = pcall(() => {
86
+ (m as unknown as { Source: string }).Source = wrapped;
87
+ });
88
+ if (!okSet) {
89
+ m.Destroy();
90
+ return {
91
+ bridge: "ok",
92
+ ok: false,
93
+ error: `ModuleScript Source set failed: ${tostring(setErr)}`,
94
+ };
95
+ }
96
+
97
+ m.Parent = game.GetService("Workspace");
98
+ const historyStart = LogService.GetLogHistory().size();
99
+ const [invokeOk, invokeResult] = pcall(() => bridge.Invoke(m) as BridgeInvokeResult);
100
+ m.Destroy();
101
+
102
+ if (!invokeOk) {
103
+ return {
104
+ bridge: "ok",
105
+ ok: false,
106
+ error: tostring(invokeResult),
107
+ };
108
+ }
109
+
110
+ if (!typeIs(invokeResult, "table")) {
111
+ return {
112
+ bridge: "ok",
113
+ ok: false,
114
+ error: `Eval bridge returned invalid result: ${tostring(invokeResult)}`,
115
+ };
116
+ }
117
+
118
+ const bridgeResult = invokeResult as BridgeInvokeResult;
119
+ if (bridgeResult.ok !== true) {
120
+ return {
121
+ bridge: "ok",
122
+ ok: false,
123
+ error: LuauExec.recoverPayloadRequireError(bridgeResult.value, userLines, PAYLOAD_INSTANCE_NAME, historyStart),
124
+ };
125
+ }
126
+
127
+ const inner = bridgeResult.value;
128
+ if (!typeIs(inner, "table")) {
129
+ return {
130
+ bridge: "ok",
131
+ ok: true,
132
+ result: inner === undefined ? undefined : LuauExec.formatReturnValue(inner),
133
+ };
134
+ }
135
+
136
+ const r = inner as WrapperResult;
137
+ const ok = r.ok === true;
138
+ return {
139
+ bridge: "ok",
140
+ ok,
141
+ result: ok && r.value !== undefined ? LuauExec.formatReturnValue(r.value) : undefined,
142
+ error: !ok ? tostring(r.value) : undefined,
143
+ output: r.output ?? [],
144
+ };
145
+ }
146
+
147
+ export = {
148
+ evalRuntime,
149
+ };
@@ -0,0 +1,160 @@
1
+ // Virtual input via UserInputService:CreateVirtualInput().
2
+ //
3
+ // We deliberately do NOT use VirtualInputManager:Send*Event — those methods
4
+ // are gated behind RobloxScriptSecurity ("lacking capability RobloxScript")
5
+ // in every context a plugin can reach (edit DM, play server/client DMs), so
6
+ // they silently never worked. CreateVirtualInput() is callable without that
7
+ // capability and drives the REAL input pipeline: SendKey feeds
8
+ // UserInputService.InputBegan/Ended and the control modules (so WASD walks the
9
+ // character at full WalkSpeed with controls intact, no Humanoid hijack),
10
+ // SendMouseButton feeds UIS and activates GUI buttons (and hit-tests against
11
+ // CoreGui), and SendTextInput types into the focused TextBox.
12
+ //
13
+ // Method set on the VirtualInput object (verified live):
14
+ // SendKey(isDown: boolean, keyCode: Enum.KeyCode)
15
+ // SendMouseButton(position: Vector2, inputType: Enum.UserInputType, isDown: boolean)
16
+ // SendTextInput(text: string)
17
+ // There is NO SendMouseMove / SendMouseWheel / SendKeyEvent — so "move" and
18
+ // "scroll" mouse actions are not supported.
19
+ //
20
+ // Coordinate space: SendMouseButton coordinates are logical viewport pixels
21
+ // (origin at the top-left of the rendered viewport). On OS-scaled displays,
22
+ // CaptureScreenshot may return a larger physical image; the MCP server reports
23
+ // the conversion from image pixels to these logical input coordinates.
24
+
25
+ import * as RenderMonitor from "../RenderMonitor";
26
+
27
+ const UserInputService = game.GetService("UserInputService");
28
+
29
+ interface VirtualInput {
30
+ SendKey(isDown: boolean, keyCode: Enum.KeyCode): void;
31
+ SendMouseButton(position: Vector2, inputType: Enum.UserInputType, isDown: boolean): void;
32
+ SendTextInput(text: string): void;
33
+ }
34
+
35
+ // One VirtualInput per plugin VM, reused across calls so that a key held down
36
+ // in one call (action="press") and released in a later call (action="release")
37
+ // share the same input source.
38
+ let cachedVI: VirtualInput | undefined;
39
+
40
+ function getVI(): VirtualInput | undefined {
41
+ if (cachedVI) return cachedVI;
42
+ const [ok, vi] = pcall(() => {
43
+ return (UserInputService as unknown as { CreateVirtualInput(): unknown }).CreateVirtualInput();
44
+ });
45
+ if (ok && vi !== undefined) {
46
+ cachedVI = vi as VirtualInput;
47
+ return cachedVI;
48
+ }
49
+ return undefined;
50
+ }
51
+
52
+ const MOUSE_TYPE_MAP: Record<string, Enum.UserInputType> = {
53
+ Left: Enum.UserInputType.MouseButton1,
54
+ Right: Enum.UserInputType.MouseButton2,
55
+ Middle: Enum.UserInputType.MouseButton3,
56
+ };
57
+
58
+ function simulateMouseInput(requestData: Record<string, unknown>) {
59
+ const action = requestData.action as string;
60
+ const x = requestData.x as number | undefined;
61
+ const y = requestData.y as number | undefined;
62
+ const button = (requestData.button as string) ?? "Left";
63
+
64
+ if (!action) return { error: "action is required" };
65
+ if (x === undefined || y === undefined) {
66
+ return { error: "x and y are required" };
67
+ }
68
+
69
+ // Input is silently dropped by the engine when the window isn't rendering
70
+ // (e.g. minimized). Surface that instead of returning a false success.
71
+ const notRendering = RenderMonitor.notRenderingReason();
72
+ if (notRendering !== undefined) return { error: notRendering };
73
+
74
+ const vi = getVI();
75
+ if (!vi) {
76
+ return { error: "UserInputService:CreateVirtualInput() is not available in this context" };
77
+ }
78
+
79
+ const inputType = MOUSE_TYPE_MAP[button] ?? Enum.UserInputType.MouseButton1;
80
+ const pos = new Vector2(x, y);
81
+
82
+ const [success, err] = pcall(() => {
83
+ if (action === "click") {
84
+ vi.SendMouseButton(pos, inputType, true);
85
+ task.wait(0.05);
86
+ vi.SendMouseButton(pos, inputType, false);
87
+ } else if (action === "mouseDown") {
88
+ vi.SendMouseButton(pos, inputType, true);
89
+ } else if (action === "mouseUp") {
90
+ vi.SendMouseButton(pos, inputType, false);
91
+ } else {
92
+ error(
93
+ `Unsupported action "${action}". CreateVirtualInput supports click, mouseDown, mouseUp ` +
94
+ `(no move/scroll — those methods don't exist on VirtualInput).`,
95
+ );
96
+ }
97
+ });
98
+
99
+ if (success) {
100
+ return { success: true, action, x, y, button };
101
+ }
102
+ return { error: `Failed to simulate mouse input: ${err}` };
103
+ }
104
+
105
+ function simulateKeyboardInput(requestData: Record<string, unknown>) {
106
+ const notRendering = RenderMonitor.notRenderingReason();
107
+ if (notRendering !== undefined) return { error: notRendering };
108
+
109
+ const vi = getVI();
110
+ if (!vi) {
111
+ return { error: "UserInputService:CreateVirtualInput() is not available in this context" };
112
+ }
113
+
114
+ // Text mode: type a string into the focused TextBox.
115
+ const text = requestData.text as string | undefined;
116
+ if (text !== undefined) {
117
+ const [ok, err] = pcall(() => vi.SendTextInput(text));
118
+ if (ok) return { success: true, text };
119
+ return { error: `Failed to send text input: ${err}` };
120
+ }
121
+
122
+ const keyCodeName = requestData.keyCode as string;
123
+ if (!keyCodeName) return { error: "keyCode (or text) is required" };
124
+
125
+ const action = (requestData.action as string) ?? "tap";
126
+ const duration = (requestData.duration as number | undefined) ?? (requestData.holdDuration as number | undefined) ?? 0.1;
127
+
128
+ const [enumOk, keyCode] = pcall(() => {
129
+ return (Enum.KeyCode as unknown as Record<string, Enum.KeyCode>)[keyCodeName];
130
+ });
131
+ if (!enumOk || !keyCode) {
132
+ return {
133
+ error: `Unknown keyCode: ${keyCodeName}. Use Enum.KeyCode names like "W", "Space", "E", "LeftShift", etc.`,
134
+ };
135
+ }
136
+
137
+ const [success, err] = pcall(() => {
138
+ if (action === "press") {
139
+ vi.SendKey(true, keyCode);
140
+ } else if (action === "release") {
141
+ vi.SendKey(false, keyCode);
142
+ } else if (action === "tap") {
143
+ vi.SendKey(true, keyCode);
144
+ task.wait(duration);
145
+ vi.SendKey(false, keyCode);
146
+ } else {
147
+ error(`Unknown action: ${action}`);
148
+ }
149
+ });
150
+
151
+ if (success) {
152
+ return { success: true, keyCode: keyCodeName, action };
153
+ }
154
+ return { error: `Failed to simulate keyboard input: ${err}` };
155
+ }
156
+
157
+ export = {
158
+ simulateMouseInput,
159
+ simulateKeyboardInput,
160
+ };
@@ -0,0 +1,380 @@
1
+ import Utils from "../Utils";
2
+ import Recording from "../Recording";
3
+
4
+ const { getInstancePath, getInstanceByPath, convertPropertyValue } = Utils;
5
+ const { beginRecording, finishRecording } = Recording;
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
+
87
+ function createObject(requestData: Record<string, unknown>) {
88
+ const className = requestData.className as string;
89
+ const parentPath = requestData.parent as string;
90
+ const name = requestData.name as string | undefined;
91
+ const properties = (requestData.properties as Record<string, unknown>) ?? {};
92
+
93
+ if (!className || !parentPath) {
94
+ return { error: "Class name and parent are required" };
95
+ }
96
+
97
+ const parentInstance = getInstanceByPath(parentPath);
98
+ if (!parentInstance) return { error: `Parent instance not found: ${parentPath}` };
99
+ const recordingId = beginRecording(`Create ${className}`);
100
+
101
+ const [success, newInstance] = pcall(() => {
102
+ const instance = new Instance(className as keyof CreatableInstances);
103
+ if (name) instance.Name = name;
104
+
105
+ for (const [propertyName, propertyValue] of pairs(properties)) {
106
+ pcall(() => {
107
+ const converted = convertPropertyValue(instance, propertyName as string, propertyValue);
108
+ (instance as unknown as { [key: string]: unknown })[propertyName as string] =
109
+ converted !== undefined ? converted : propertyValue;
110
+ });
111
+ }
112
+
113
+ instance.Parent = parentInstance;
114
+ return instance;
115
+ });
116
+
117
+ if (success && newInstance) {
118
+ finishRecording(recordingId, true);
119
+ return {
120
+ success: true,
121
+ className,
122
+ parent: parentPath,
123
+ instancePath: getInstancePath(newInstance as Instance),
124
+ name: (newInstance as Instance).Name,
125
+ message: "Object created successfully",
126
+ };
127
+ } else {
128
+ finishRecording(recordingId, false);
129
+ return { error: `Failed to create object: ${newInstance}`, className, parent: parentPath };
130
+ }
131
+ }
132
+
133
+ function deleteObject(requestData: Record<string, unknown>) {
134
+ const instancePath = requestData.instancePath as string;
135
+ if (!instancePath) return { error: "Instance path is required" };
136
+
137
+ const instance = getInstanceByPath(instancePath);
138
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
139
+ if (instance === game) return { error: "Cannot delete the game instance" };
140
+ const recordingId = beginRecording(`Delete ${instance.ClassName} (${instance.Name})`);
141
+
142
+ const [success, result] = pcall(() => {
143
+ instance.Destroy();
144
+ return true;
145
+ });
146
+
147
+ if (success) {
148
+ finishRecording(recordingId, true);
149
+ return { success: true, instancePath, message: "Object deleted successfully" };
150
+ } else {
151
+ finishRecording(recordingId, false);
152
+ return { error: `Failed to delete object: ${result}`, instancePath };
153
+ }
154
+ }
155
+
156
+ function massCreateObjects(requestData: Record<string, unknown>) {
157
+ const objects = requestData.objects as Record<string, unknown>[];
158
+ if (!objects || !typeIs(objects, "table") || (objects as defined[]).size() === 0) {
159
+ return { error: "Objects array is required" };
160
+ }
161
+
162
+ const recordingId = beginRecording("Mass create objects");
163
+
164
+ const { results, successCount, failureCount } = processObjectEntries(objects, (objData) => {
165
+ const className = objData.className as string;
166
+ const parentPath = objData.parent as string;
167
+ const name = objData.name as string | undefined;
168
+ const properties = (objData.properties as Record<string, unknown>) ?? {};
169
+ const parentInstance = getInstanceByPath(parentPath);
170
+ if (!parentInstance) {
171
+ return { error: "Parent instance not found", className, parentPath };
172
+ }
173
+
174
+ const [success, newInstance] = pcall(() => {
175
+ const instance = new Instance(className as keyof CreatableInstances);
176
+ if (name) instance.Name = name;
177
+
178
+ for (const [propertyName, propertyValue] of pairs(properties)) {
179
+ pcall(() => {
180
+ const converted = convertPropertyValue(instance, propertyName as string, propertyValue);
181
+ (instance as unknown as { [key: string]: unknown })[propertyName as string] =
182
+ converted !== undefined ? converted : propertyValue;
183
+ });
184
+ }
185
+
186
+ instance.Parent = parentInstance;
187
+ return instance;
188
+ });
189
+
190
+ if (!success || !newInstance) {
191
+ return { error: tostring(newInstance), className, parentPath };
192
+ }
193
+
194
+ return { instance: newInstance as Instance, className, parentPath };
195
+ });
196
+
197
+ finishRecording(recordingId, successCount > 0);
198
+ return { results, summary: { total: (objects as defined[]).size(), succeeded: successCount, failed: failureCount } };
199
+ }
200
+
201
+
202
+
203
+ function performSmartDuplicate(requestData: Record<string, unknown>, useRecording = true) {
204
+ const instancePath = requestData.instancePath as string;
205
+ const count = requestData.count as number;
206
+ const options = (requestData.options as Record<string, unknown>) ?? {};
207
+
208
+ if (!instancePath || !count || count < 1) {
209
+ return { error: "Instance path and count > 0 are required" };
210
+ }
211
+
212
+ const instance = getInstanceByPath(instancePath);
213
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
214
+ const recordingId = useRecording ? beginRecording(`Smart duplicate ${instance.Name}`) : undefined;
215
+
216
+ const results: Record<string, unknown>[] = [];
217
+ let successCount = 0;
218
+ let failureCount = 0;
219
+
220
+ for (let i = 1; i <= count; i++) {
221
+ const [success, newInstance] = pcall(() => {
222
+ const clone = instance.Clone();
223
+
224
+ if (options.namePattern) {
225
+ clone.Name = (options.namePattern as string).gsub("{n}", tostring(i))[0];
226
+ } else {
227
+ clone.Name = instance.Name + i;
228
+ }
229
+
230
+ if (options.positionOffset && clone.IsA("BasePart")) {
231
+ const offset = options.positionOffset as number[];
232
+ const currentPos = clone.Position;
233
+ clone.Position = new Vector3(
234
+ currentPos.X + ((offset[0] ?? 0) as number) * i,
235
+ currentPos.Y + ((offset[1] ?? 0) as number) * i,
236
+ currentPos.Z + ((offset[2] ?? 0) as number) * i,
237
+ );
238
+ }
239
+
240
+ if (options.rotationOffset && clone.IsA("BasePart")) {
241
+ const offset = options.rotationOffset as number[];
242
+ clone.CFrame = clone.CFrame.mul(CFrame.Angles(
243
+ math.rad(((offset[0] ?? 0) as number) * i),
244
+ math.rad(((offset[1] ?? 0) as number) * i),
245
+ math.rad(((offset[2] ?? 0) as number) * i),
246
+ ));
247
+ }
248
+
249
+ if (options.scaleOffset && clone.IsA("BasePart")) {
250
+ const offset = options.scaleOffset as number[];
251
+ const currentSize = clone.Size;
252
+ clone.Size = new Vector3(
253
+ currentSize.X * (((offset[0] ?? 1) as number) ** i),
254
+ currentSize.Y * (((offset[1] ?? 1) as number) ** i),
255
+ currentSize.Z * (((offset[2] ?? 1) as number) ** i),
256
+ );
257
+ }
258
+
259
+ if (options.propertyVariations) {
260
+ for (const [propName, values] of pairs(options.propertyVariations as Record<string, unknown[]>)) {
261
+ if (values && (values as defined[]).size() > 0) {
262
+ const valueIndex = ((i - 1) % (values as defined[]).size());
263
+ pcall(() => {
264
+ (clone as unknown as { [key: string]: unknown })[propName as string] = (values as unknown[])[valueIndex];
265
+ });
266
+ }
267
+ }
268
+ }
269
+
270
+ const targetParents = options.targetParents as string[] | undefined;
271
+ if (targetParents && targetParents[i - 1]) {
272
+ const targetParent = getInstanceByPath(targetParents[i - 1]);
273
+ clone.Parent = targetParent ?? instance.Parent;
274
+ } else {
275
+ clone.Parent = instance.Parent;
276
+ }
277
+
278
+ return clone;
279
+ });
280
+
281
+ if (success && newInstance) {
282
+ successCount++;
283
+ results.push({
284
+ success: true,
285
+ instancePath: getInstancePath(newInstance as Instance),
286
+ name: (newInstance as Instance).Name,
287
+ index: i,
288
+ });
289
+ } else {
290
+ failureCount++;
291
+ results.push({ success: false, index: i, error: tostring(newInstance) });
292
+ }
293
+ }
294
+
295
+ finishRecording(recordingId, successCount > 0);
296
+
297
+ return {
298
+ results,
299
+ summary: { total: count, succeeded: successCount, failed: failureCount },
300
+ sourceInstance: instancePath,
301
+ };
302
+ }
303
+
304
+ function smartDuplicate(requestData: Record<string, unknown>) {
305
+ return performSmartDuplicate(requestData, true);
306
+ }
307
+
308
+ function massDuplicate(requestData: Record<string, unknown>) {
309
+ const duplications = requestData.duplications as Record<string, unknown>[];
310
+ if (!duplications || !typeIs(duplications, "table") || (duplications as defined[]).size() === 0) {
311
+ return { error: "Duplications array is required" };
312
+ }
313
+
314
+ const allResults: Record<string, unknown>[] = [];
315
+ let totalSuccess = 0;
316
+ let totalFailures = 0;
317
+ const recordingId = beginRecording("Mass duplicate operations");
318
+
319
+ for (const duplication of duplications) {
320
+ const result = performSmartDuplicate(duplication, false) as { summary?: { succeeded: number; failed: number } };
321
+ allResults.push(result as unknown as Record<string, unknown>);
322
+ if (result.summary) {
323
+ totalSuccess += result.summary.succeeded;
324
+ totalFailures += result.summary.failed;
325
+ }
326
+ }
327
+
328
+ finishRecording(recordingId, totalSuccess > 0);
329
+
330
+ return {
331
+ results: allResults,
332
+ summary: { total: totalSuccess + totalFailures, succeeded: totalSuccess, failed: totalFailures },
333
+ };
334
+ }
335
+
336
+ function cloneObject(requestData: Record<string, unknown>) {
337
+ const instancePath = requestData.instancePath as string;
338
+ const targetParentPath = requestData.targetParentPath as string;
339
+
340
+ if (!instancePath || !targetParentPath) {
341
+ return { error: "Instance path and target parent path are required" };
342
+ }
343
+
344
+ const instance = getInstanceByPath(instancePath);
345
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
346
+
347
+ const targetParent = getInstanceByPath(targetParentPath);
348
+ if (!targetParent) return { error: `Target parent not found: ${targetParentPath}` };
349
+
350
+ const recordingId = beginRecording(`Clone ${instance.Name}`);
351
+
352
+ const [success, clone] = pcall(() => {
353
+ const cloned = instance.Clone();
354
+ cloned.Parent = targetParent;
355
+ return cloned;
356
+ });
357
+
358
+ if (success && clone) {
359
+ finishRecording(recordingId, true);
360
+ return {
361
+ success: true,
362
+ instancePath: getInstancePath(clone as Instance),
363
+ name: (clone as Instance).Name,
364
+ className: (clone as Instance).ClassName,
365
+ parent: targetParentPath,
366
+ message: "Object cloned successfully",
367
+ };
368
+ }
369
+ finishRecording(recordingId, false);
370
+ return { error: `Failed to clone object: ${clone}` };
371
+ }
372
+
373
+ export = {
374
+ createObject,
375
+ deleteObject,
376
+ massCreateObjects,
377
+ smartDuplicate,
378
+ massDuplicate,
379
+ cloneObject,
380
+ };