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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robloxstudio-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "MCP Server for Roblox Studio Integration - Access Studio data, scripts, and objects through AI tools",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,23 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/**/*",
|
|
12
|
-
"studio-plugin/**/*"
|
|
13
|
-
"README.md"
|
|
12
|
+
"studio-plugin/**/*"
|
|
14
13
|
],
|
|
15
14
|
"scripts": {
|
|
16
|
-
"build": "
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"dev": "tsx src/index.ts",
|
|
20
|
-
"start": "node dist/index.js",
|
|
21
|
-
"lint": "eslint src/**/*.ts",
|
|
22
|
-
"typecheck": "tsc --noEmit",
|
|
23
|
-
"test": "jest",
|
|
24
|
-
"test:watch": "jest --watch",
|
|
25
|
-
"test:coverage": "jest --coverage",
|
|
26
|
-
"test:e2e": "lune run tests/luau/e2e.luau",
|
|
27
|
-
"test:all": "npm test && npm run test:e2e",
|
|
28
|
-
"prepublishOnly": "npm run build:all"
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"prepack": "node ../../scripts/prepack.mjs",
|
|
17
|
+
"postpack": "node ../../scripts/postpack.mjs"
|
|
29
18
|
},
|
|
30
19
|
"keywords": [
|
|
31
20
|
"mcp",
|
|
@@ -54,20 +43,6 @@
|
|
|
54
43
|
"ws": "^8.14.2"
|
|
55
44
|
},
|
|
56
45
|
"devDependencies": {
|
|
57
|
-
"@
|
|
58
|
-
"@types/express": "^4.17.21",
|
|
59
|
-
"@types/jest": "^29.5.11",
|
|
60
|
-
"@types/node": "^20.10.0",
|
|
61
|
-
"@types/supertest": "^6.0.2",
|
|
62
|
-
"@types/uuid": "^9.0.7",
|
|
63
|
-
"@types/ws": "^8.5.10",
|
|
64
|
-
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
65
|
-
"@typescript-eslint/parser": "^7.0.0",
|
|
66
|
-
"eslint": "^8.57.0",
|
|
67
|
-
"jest": "^29.7.0",
|
|
68
|
-
"supertest": "^6.3.3",
|
|
69
|
-
"ts-jest": "^29.1.1",
|
|
70
|
-
"tsx": "^4.6.0",
|
|
71
|
-
"typescript": "^5.3.2"
|
|
46
|
+
"@robloxstudio-mcp/core": "*"
|
|
72
47
|
}
|
|
73
48
|
}
|
|
@@ -440,7 +440,7 @@ local function checkForUpdates()
|
|
|
440
440
|
local latestVersion = data.version
|
|
441
441
|
if Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0 then
|
|
442
442
|
local ui = UI.getElements()
|
|
443
|
-
ui.updateBannerText.Text = `
|
|
443
|
+
ui.updateBannerText.Text = `v{latestVersion} available - github.com/boshyxd/robloxstudio-mcp`
|
|
444
444
|
ui.updateBanner.Visible = true
|
|
445
445
|
ui.contentFrame.Position = UDim2.new(0, 8, 0, 92)
|
|
446
446
|
ui.contentFrame.Size = UDim2.new(1, -16, 1, -100)
|
|
@@ -3173,7 +3173,7 @@ return {
|
|
|
3173
3173
|
<Properties>
|
|
3174
3174
|
<string name="Name">State</string>
|
|
3175
3175
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
3176
|
-
local CURRENT_VERSION = "2.
|
|
3176
|
+
local CURRENT_VERSION = "2.4.0"
|
|
3177
3177
|
local MAX_CONNECTIONS = 5
|
|
3178
3178
|
local BASE_PORT = 58741
|
|
3179
3179
|
local activeTabIndex = 0
|
|
@@ -3606,28 +3606,15 @@ local function init(pluginRef)
|
|
|
3606
3606
|
updateBannerCorner.CornerRadius = UDim.new(0, 3)
|
|
3607
3607
|
updateBannerCorner.Parent = updateBanner
|
|
3608
3608
|
local updateBannerText = Instance.new("TextLabel")
|
|
3609
|
-
updateBannerText.Size = UDim2.new(1, -
|
|
3609
|
+
updateBannerText.Size = UDim2.new(1, -16, 1, 0)
|
|
3610
3610
|
updateBannerText.Position = UDim2.new(0, 8, 0, 0)
|
|
3611
3611
|
updateBannerText.BackgroundTransparency = 1
|
|
3612
|
-
updateBannerText.Text = "
|
|
3612
|
+
updateBannerText.Text = ""
|
|
3613
3613
|
updateBannerText.TextColor3 = C.yellow
|
|
3614
3614
|
updateBannerText.TextSize = 9
|
|
3615
3615
|
updateBannerText.Font = Enum.Font.GothamMedium
|
|
3616
3616
|
updateBannerText.TextXAlignment = Enum.TextXAlignment.Left
|
|
3617
3617
|
updateBannerText.Parent = updateBanner
|
|
3618
|
-
local updateLink = Instance.new("TextButton")
|
|
3619
|
-
updateLink.Size = UDim2.new(0, 40, 0, 16)
|
|
3620
|
-
updateLink.Position = UDim2.new(1, -46, 0, 4)
|
|
3621
|
-
updateLink.BackgroundColor3 = C.yellow
|
|
3622
|
-
updateLink.BackgroundTransparency = 0.85
|
|
3623
|
-
updateLink.Text = "Get"
|
|
3624
|
-
updateLink.TextColor3 = C.yellow
|
|
3625
|
-
updateLink.TextSize = 9
|
|
3626
|
-
updateLink.Font = Enum.Font.GothamBold
|
|
3627
|
-
updateLink.Parent = updateBanner
|
|
3628
|
-
local updateLinkCorner = Instance.new("UICorner")
|
|
3629
|
-
updateLinkCorner.CornerRadius = UDim.new(0, 3)
|
|
3630
|
-
updateLinkCorner.Parent = updateLink
|
|
3631
3618
|
local contentY = 66
|
|
3632
3619
|
local contentFrame = Instance.new("ScrollingFrame")
|
|
3633
3620
|
contentFrame.Size = UDim2.new(1, -16, 1, -(contentY + 8))
|
|
@@ -3810,22 +3797,6 @@ local function init(pluginRef)
|
|
|
3810
3797
|
setButtonConnect(connectButton, connectStroke)
|
|
3811
3798
|
end
|
|
3812
3799
|
end)
|
|
3813
|
-
updateLink.Activated:Connect(function()
|
|
3814
|
-
local url = "https://github.com/boshyxd/robloxstudio-mcp/releases"
|
|
3815
|
-
local env = getfenv(0)
|
|
3816
|
-
local _value = env.setclipboard
|
|
3817
|
-
if _value ~= 0 and _value == _value and _value ~= "" and _value then
|
|
3818
|
-
pcall(function()
|
|
3819
|
-
env.setclipboard(url)
|
|
3820
|
-
end)
|
|
3821
|
-
end
|
|
3822
|
-
updateBannerText.Text = "Link copied!"
|
|
3823
|
-
task.delay(3, function()
|
|
3824
|
-
if updateBanner.Visible then
|
|
3825
|
-
updateBannerText.Text = "Update available - check GitHub"
|
|
3826
|
-
end
|
|
3827
|
-
end)
|
|
3828
|
-
end)
|
|
3829
3800
|
elements = {
|
|
3830
3801
|
screenGui = screenGui,
|
|
3831
3802
|
mainFrame = mainFrame,
|
|
@@ -8,6 +8,8 @@ import InstanceHandlers from "./handlers/InstanceHandlers";
|
|
|
8
8
|
import ScriptHandlers from "./handlers/ScriptHandlers";
|
|
9
9
|
import MetadataHandlers from "./handlers/MetadataHandlers";
|
|
10
10
|
import TestHandlers from "./handlers/TestHandlers";
|
|
11
|
+
import BuildHandlers from "./handlers/BuildHandlers";
|
|
12
|
+
import AssetHandlers from "./handlers/AssetHandlers";
|
|
11
13
|
import { Connection, RequestPayload, PollResponse } from "../types";
|
|
12
14
|
|
|
13
15
|
type Handler = (data: Record<string, unknown>) => unknown;
|
|
@@ -24,6 +26,7 @@ const routeMap: Record<string, Handler> = {
|
|
|
24
26
|
"/api/search-by-property": QueryHandlers.searchByProperty,
|
|
25
27
|
"/api/class-info": QueryHandlers.getClassInfo,
|
|
26
28
|
"/api/project-structure": QueryHandlers.getProjectStructure,
|
|
29
|
+
"/api/grep-scripts": QueryHandlers.grepScripts,
|
|
27
30
|
|
|
28
31
|
"/api/set-property": PropertyHandlers.setProperty,
|
|
29
32
|
"/api/mass-set-property": PropertyHandlers.massSetProperty,
|
|
@@ -54,10 +57,20 @@ const routeMap: Record<string, Handler> = {
|
|
|
54
57
|
"/api/get-tagged": MetadataHandlers.getTagged,
|
|
55
58
|
"/api/get-selection": MetadataHandlers.getSelection,
|
|
56
59
|
"/api/execute-luau": MetadataHandlers.executeLuau,
|
|
60
|
+
"/api/undo": MetadataHandlers.undo,
|
|
61
|
+
"/api/redo": MetadataHandlers.redo,
|
|
57
62
|
|
|
58
63
|
"/api/start-playtest": TestHandlers.startPlaytest,
|
|
59
64
|
"/api/stop-playtest": TestHandlers.stopPlaytest,
|
|
60
65
|
"/api/get-playtest-output": TestHandlers.getPlaytestOutput,
|
|
66
|
+
|
|
67
|
+
"/api/export-build": BuildHandlers.exportBuild,
|
|
68
|
+
"/api/import-build": BuildHandlers.importBuild,
|
|
69
|
+
"/api/import-scene": BuildHandlers.importScene,
|
|
70
|
+
"/api/search-materials": BuildHandlers.searchMaterials,
|
|
71
|
+
|
|
72
|
+
"/api/insert-asset": AssetHandlers.insertAsset,
|
|
73
|
+
"/api/preview-asset": AssetHandlers.previewAsset,
|
|
61
74
|
};
|
|
62
75
|
|
|
63
76
|
function processRequest(request: RequestPayload): unknown {
|
|
@@ -365,7 +378,7 @@ function checkForUpdates() {
|
|
|
365
378
|
const latestVersion = data.version;
|
|
366
379
|
if (Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0) {
|
|
367
380
|
const ui = UI.getElements();
|
|
368
|
-
ui.updateBannerText.Text = `
|
|
381
|
+
ui.updateBannerText.Text = `v${latestVersion} available - github.com/boshyxd/robloxstudio-mcp`;
|
|
369
382
|
ui.updateBanner.Visible = true;
|
|
370
383
|
ui.contentFrame.Position = new UDim2(0, 8, 0, 92);
|
|
371
384
|
ui.contentFrame.Size = new UDim2(1, -16, 1, -100);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const ChangeHistoryService = game.GetService("ChangeHistoryService");
|
|
2
|
+
|
|
3
|
+
type RecordingId = string | undefined;
|
|
4
|
+
|
|
5
|
+
function beginRecording(actionName: string): RecordingId {
|
|
6
|
+
const [success, result] = pcall(() => ChangeHistoryService.TryBeginRecording(`MCP: ${actionName}`));
|
|
7
|
+
if (success) {
|
|
8
|
+
return result as RecordingId;
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function finishRecording(recordingId: RecordingId, shouldCommit: boolean) {
|
|
14
|
+
if (recordingId === undefined) return;
|
|
15
|
+
|
|
16
|
+
const operation = shouldCommit
|
|
17
|
+
? Enum.FinishRecordingOperation.Commit
|
|
18
|
+
: Enum.FinishRecordingOperation.Cancel;
|
|
19
|
+
|
|
20
|
+
pcall(() => {
|
|
21
|
+
ChangeHistoryService.FinishRecording(recordingId, operation);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export = {
|
|
26
|
+
beginRecording,
|
|
27
|
+
finishRecording,
|
|
28
|
+
};
|
|
@@ -362,31 +362,16 @@ function init(pluginRef: Plugin) {
|
|
|
362
362
|
updateBannerCorner.Parent = updateBanner;
|
|
363
363
|
|
|
364
364
|
const updateBannerText = new Instance("TextLabel");
|
|
365
|
-
updateBannerText.Size = new UDim2(1, -
|
|
365
|
+
updateBannerText.Size = new UDim2(1, -16, 1, 0);
|
|
366
366
|
updateBannerText.Position = new UDim2(0, 8, 0, 0);
|
|
367
367
|
updateBannerText.BackgroundTransparency = 1;
|
|
368
|
-
updateBannerText.Text = "
|
|
368
|
+
updateBannerText.Text = "";
|
|
369
369
|
updateBannerText.TextColor3 = C.yellow;
|
|
370
370
|
updateBannerText.TextSize = 9;
|
|
371
371
|
updateBannerText.Font = Enum.Font.GothamMedium;
|
|
372
372
|
updateBannerText.TextXAlignment = Enum.TextXAlignment.Left;
|
|
373
373
|
updateBannerText.Parent = updateBanner;
|
|
374
374
|
|
|
375
|
-
const updateLink = new Instance("TextButton");
|
|
376
|
-
updateLink.Size = new UDim2(0, 40, 0, 16);
|
|
377
|
-
updateLink.Position = new UDim2(1, -46, 0, 4);
|
|
378
|
-
updateLink.BackgroundColor3 = C.yellow;
|
|
379
|
-
updateLink.BackgroundTransparency = 0.85;
|
|
380
|
-
updateLink.Text = "Get";
|
|
381
|
-
updateLink.TextColor3 = C.yellow;
|
|
382
|
-
updateLink.TextSize = 9;
|
|
383
|
-
updateLink.Font = Enum.Font.GothamBold;
|
|
384
|
-
updateLink.Parent = updateBanner;
|
|
385
|
-
|
|
386
|
-
const updateLinkCorner = new Instance("UICorner");
|
|
387
|
-
updateLinkCorner.CornerRadius = new UDim(0, 3);
|
|
388
|
-
updateLinkCorner.Parent = updateLink;
|
|
389
|
-
|
|
390
375
|
const contentY = 66;
|
|
391
376
|
const contentFrame = new Instance("ScrollingFrame");
|
|
392
377
|
contentFrame.Size = new UDim2(1, -16, 1, -(contentY + 8));
|
|
@@ -580,21 +565,6 @@ function init(pluginRef: Plugin) {
|
|
|
580
565
|
}
|
|
581
566
|
});
|
|
582
567
|
|
|
583
|
-
updateLink.Activated.Connect(() => {
|
|
584
|
-
const url = "https://github.com/boshyxd/robloxstudio-mcp/releases";
|
|
585
|
-
const env = getfenv(0) as unknown as Record<string, unknown>;
|
|
586
|
-
if (env["setclipboard"]) {
|
|
587
|
-
pcall(() => {
|
|
588
|
-
(env["setclipboard"] as (s: string) => void)(url);
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
updateBannerText.Text = "Link copied!";
|
|
592
|
-
task.delay(3, () => {
|
|
593
|
-
if (updateBanner.Visible) {
|
|
594
|
-
updateBannerText.Text = "Update available - check GitHub";
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
});
|
|
598
568
|
|
|
599
569
|
elements = {
|
|
600
570
|
screenGui, mainFrame, contentFrame, statusLabel, detailStatusLabel,
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import Utils from "../Utils";
|
|
2
|
+
import Recording from "../Recording";
|
|
3
|
+
|
|
4
|
+
const AssetService = game.GetService("AssetService");
|
|
5
|
+
const ChangeHistoryService = game.GetService("ChangeHistoryService");
|
|
6
|
+
const Selection = game.GetService("Selection");
|
|
7
|
+
|
|
8
|
+
const { getInstancePath, getInstanceByPath } = Utils;
|
|
9
|
+
const { beginRecording, finishRecording } = Recording;
|
|
10
|
+
|
|
11
|
+
function insertAsset(requestData: Record<string, unknown>) {
|
|
12
|
+
const assetId = requestData.assetId as number;
|
|
13
|
+
const parentPath = (requestData.parentPath as string) ?? "game.Workspace";
|
|
14
|
+
const position = requestData.position as { x: number; y: number; z: number } | undefined;
|
|
15
|
+
|
|
16
|
+
if (!assetId) {
|
|
17
|
+
return { error: "assetId is required" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const parentInstance = getInstanceByPath(parentPath);
|
|
21
|
+
if (!parentInstance) {
|
|
22
|
+
return { error: `Parent instance not found: ${parentPath}` };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const recordingId = beginRecording(`Insert asset ${assetId}`);
|
|
26
|
+
|
|
27
|
+
const [loadSuccess, wrapperModel] = pcall(() => {
|
|
28
|
+
return (AssetService as unknown as { LoadAssetAsync(id: number): Instance }).LoadAssetAsync(assetId);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!loadSuccess || !wrapperModel) {
|
|
32
|
+
finishRecording(recordingId, false);
|
|
33
|
+
return { error: `Failed to load asset ${assetId}: ${tostring(wrapperModel)}` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const insertedInstances: Instance[] = [];
|
|
37
|
+
const children = (wrapperModel as Instance).GetChildren();
|
|
38
|
+
|
|
39
|
+
for (const child of children) {
|
|
40
|
+
child.Parent = parentInstance;
|
|
41
|
+
insertedInstances.push(child);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Apply position if provided
|
|
45
|
+
if (position) {
|
|
46
|
+
const pos = new Vector3(position.x ?? 0, position.y ?? 0, position.z ?? 0);
|
|
47
|
+
for (const inst of insertedInstances) {
|
|
48
|
+
if (inst.IsA("BasePart")) {
|
|
49
|
+
inst.Position = pos;
|
|
50
|
+
} else if (inst.IsA("Model")) {
|
|
51
|
+
if (inst.PrimaryPart) {
|
|
52
|
+
inst.PivotTo(new CFrame(pos));
|
|
53
|
+
} else {
|
|
54
|
+
// Find first BasePart descendant to pivot around
|
|
55
|
+
const firstPart = inst.FindFirstChildWhichIsA("BasePart", true);
|
|
56
|
+
if (firstPart) {
|
|
57
|
+
inst.PivotTo(new CFrame(pos));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Destroy the wrapper model
|
|
65
|
+
(wrapperModel as Instance).Destroy();
|
|
66
|
+
|
|
67
|
+
// Set undo waypoint
|
|
68
|
+
finishRecording(recordingId, true);
|
|
69
|
+
|
|
70
|
+
// Select inserted instances
|
|
71
|
+
pcall(() => {
|
|
72
|
+
Selection.Set(insertedInstances);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const resultInstances = insertedInstances.map((inst) => ({
|
|
76
|
+
name: inst.Name,
|
|
77
|
+
className: inst.ClassName,
|
|
78
|
+
path: getInstancePath(inst),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
assetId,
|
|
84
|
+
parentPath,
|
|
85
|
+
insertedCount: insertedInstances.size(),
|
|
86
|
+
instances: resultInstances,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function previewAsset(requestData: Record<string, unknown>) {
|
|
91
|
+
const assetId = requestData.assetId as number;
|
|
92
|
+
const includeProperties = (requestData.includeProperties as boolean) ?? true;
|
|
93
|
+
const maxDepth = (requestData.maxDepth as number) ?? 10;
|
|
94
|
+
|
|
95
|
+
if (!assetId) {
|
|
96
|
+
return { error: "assetId is required" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const [loadSuccess, wrapperModel] = pcall(() => {
|
|
100
|
+
return (AssetService as unknown as { LoadAssetAsync(id: number): Instance }).LoadAssetAsync(assetId);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!loadSuccess || !wrapperModel) {
|
|
104
|
+
return { error: `Failed to load asset ${assetId}: ${tostring(wrapperModel)}` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Stats tracking
|
|
108
|
+
let totalInstances = 0;
|
|
109
|
+
const classCounts: Record<string, number> = {};
|
|
110
|
+
let hasScripts = false;
|
|
111
|
+
let hasAnimations = false;
|
|
112
|
+
let hasSounds = false;
|
|
113
|
+
let hasParticles = false;
|
|
114
|
+
|
|
115
|
+
function buildHierarchy(instance: Instance, depth: number): Record<string, unknown> {
|
|
116
|
+
totalInstances++;
|
|
117
|
+
|
|
118
|
+
const className = instance.ClassName;
|
|
119
|
+
classCounts[className] = (classCounts[className] ?? 0) + 1;
|
|
120
|
+
|
|
121
|
+
if (instance.IsA("LuaSourceContainer")) hasScripts = true;
|
|
122
|
+
if (className === "Animation" || className === "AnimationController" || className === "Animator") hasAnimations = true;
|
|
123
|
+
if (instance.IsA("Sound")) hasSounds = true;
|
|
124
|
+
if (className === "ParticleEmitter" || className === "Fire" || className === "Smoke" || className === "Sparkles") hasParticles = true;
|
|
125
|
+
|
|
126
|
+
const node: Record<string, unknown> = {
|
|
127
|
+
name: instance.Name,
|
|
128
|
+
className,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (includeProperties) {
|
|
132
|
+
const props: Record<string, unknown> = {};
|
|
133
|
+
|
|
134
|
+
if (instance.IsA("BasePart")) {
|
|
135
|
+
props.size = { x: instance.Size.X, y: instance.Size.Y, z: instance.Size.Z };
|
|
136
|
+
props.position = { x: instance.Position.X, y: instance.Position.Y, z: instance.Position.Z };
|
|
137
|
+
props.material = tostring(instance.Material);
|
|
138
|
+
props.color = `${instance.Color.R}, ${instance.Color.G}, ${instance.Color.B}`;
|
|
139
|
+
props.transparency = instance.Transparency;
|
|
140
|
+
props.anchored = instance.Anchored;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (instance.IsA("MeshPart")) {
|
|
144
|
+
const meshPart = instance as MeshPart;
|
|
145
|
+
props.meshId = meshPart.MeshId;
|
|
146
|
+
props.textureId = meshPart.TextureID;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (instance.IsA("Model")) {
|
|
150
|
+
const model = instance as Model;
|
|
151
|
+
if (model.PrimaryPart) {
|
|
152
|
+
props.primaryPart = model.PrimaryPart.Name;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (instance.IsA("LuaSourceContainer")) {
|
|
157
|
+
const [ok, src] = pcall(() => (instance as unknown as { Source: string }).Source);
|
|
158
|
+
if (ok && src) {
|
|
159
|
+
const preview = string.sub(src, 1, 200);
|
|
160
|
+
props.sourcePreview = preview;
|
|
161
|
+
props.sourceLength = src.size();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (className === "Decal" || className === "Texture") {
|
|
166
|
+
const [ok, texId] = pcall(() => (instance as unknown as { Texture: string }).Texture);
|
|
167
|
+
if (ok) props.texture = texId;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (instance.IsA("Sound")) {
|
|
171
|
+
props.soundId = (instance as Sound).SoundId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Only include props if there are any
|
|
175
|
+
let hasProps = false;
|
|
176
|
+
for (const _ of pairs(props)) {
|
|
177
|
+
hasProps = true;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (hasProps) {
|
|
181
|
+
node.properties = props;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (depth < maxDepth) {
|
|
186
|
+
const childNodes: Record<string, unknown>[] = [];
|
|
187
|
+
for (const child of instance.GetChildren()) {
|
|
188
|
+
childNodes.push(buildHierarchy(child, depth + 1));
|
|
189
|
+
}
|
|
190
|
+
if (childNodes.size() > 0) {
|
|
191
|
+
node.children = childNodes;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
const childCount = instance.GetChildren().size();
|
|
195
|
+
if (childCount > 0) {
|
|
196
|
+
node.childCount = childCount;
|
|
197
|
+
node.truncated = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return node;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const hierarchyRoots: Record<string, unknown>[] = [];
|
|
205
|
+
for (const child of (wrapperModel as Instance).GetChildren()) {
|
|
206
|
+
hierarchyRoots.push(buildHierarchy(child, 0));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// CRITICAL: Destroy wrapper to prevent memory leak
|
|
210
|
+
(wrapperModel as Instance).Destroy();
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
success: true,
|
|
214
|
+
assetId,
|
|
215
|
+
hierarchy: hierarchyRoots,
|
|
216
|
+
summary: {
|
|
217
|
+
totalInstances,
|
|
218
|
+
classCounts,
|
|
219
|
+
hasScripts,
|
|
220
|
+
hasAnimations,
|
|
221
|
+
hasSounds,
|
|
222
|
+
hasParticles,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export = {
|
|
228
|
+
insertAsset,
|
|
229
|
+
previewAsset,
|
|
230
|
+
};
|