feat(schematics): add schematic search/build with Playwright browser support
Some checks failed
Deploy to Docker / deploy (push) Failing after 33s

Switch Docker base image from node:22-alpine to Playwright Noble for
in-container Chromium support. Add persistent cache volume for schematics.
New files: schematics browser, cache, Java block ID mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 00:07:49 +00:00
parent 911471b564
commit 2e91bcf63d
12 changed files with 1723 additions and 3 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ node_modules/
.env .env
*.log *.log
.DS_Store .DS_Store
cache/

View File

@@ -1,4 +1,4 @@
FROM node:22-alpine FROM mcr.microsoft.com/playwright/javascript:v1.58.2-noble
WORKDIR /app WORKDIR /app
@@ -7,8 +7,11 @@ RUN npm ci --production 2>/dev/null || npm install --production
COPY src/ ./src/ COPY src/ ./src/
# Create cache directory owned by pwuser (default user in playwright image)
RUN mkdir -p /app/cache && chown -R pwuser:pwuser /app/cache
EXPOSE 3001 3002 EXPOSE 3001 3002
USER node USER pwuser
CMD ["node", "src/index.js"] CMD ["node", "src/index.js"]

View File

@@ -9,4 +9,9 @@ services:
- WS_PORT=3001 - WS_PORT=3001
- MCP_PORT=3002 - MCP_PORT=3002
- NODE_ENV=production - NODE_ENV=production
volumes:
- schematic-cache:/app/cache
restart: unless-stopped restart: unless-stopped
volumes:
schematic-cache:

432
package-lock.json generated
View File

@@ -10,6 +10,9 @@
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1", "@modelcontextprotocol/sdk": "^1.27.1",
"express": "^4.21.2", "express": "^4.21.2",
"minecraft-data": "^3.105.0",
"playwright": "^1.58.2",
"prismarine-schematic": "^1.2.3",
"ws": "^8.18.0", "ws": "^8.18.0",
"zod": "^3.25.0" "zod": "^3.25.0"
}, },
@@ -354,6 +357,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -406,6 +421,26 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.4", "version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@@ -445,6 +480,30 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -483,6 +542,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -578,6 +643,12 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/discontinuous-range": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
"integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==",
"license": "MIT"
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -652,6 +723,24 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/eventsource": { "node_modules/eventsource": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
@@ -743,6 +832,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"license": "MIT"
},
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
@@ -795,6 +890,20 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -918,6 +1027,26 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -975,6 +1104,12 @@
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/lodash.reduce": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
"integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==",
"license": "MIT"
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1044,12 +1179,55 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/minecraft-data": {
"version": "3.105.0",
"resolved": "https://registry.npmjs.org/minecraft-data/-/minecraft-data-3.105.0.tgz",
"integrity": "sha512-4bu0PYcd7qFDmLHYA0wzFYS9jqO4EpbbD4ntzdNg/wsLgqpQ/Mku8UbQcQFdap0X2zN+7Eiio0GYq2SOEoOCfg==",
"license": "MIT"
},
"node_modules/mojangson": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/mojangson/-/mojangson-2.0.4.tgz",
"integrity": "sha512-HYmhgDjr1gzF7trGgvcC/huIg2L8FsVbi/KacRe6r1AswbboGVZDS47SOZlomPuMWvZLas8m9vuHHucdZMwTmQ==",
"license": "MIT",
"dependencies": {
"nearley": "^2.19.5"
}
},
"node_modules/moo": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz",
"integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==",
"license": "BSD-3-Clause"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/nearley": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz",
"integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==",
"license": "MIT",
"dependencies": {
"commander": "^2.19.0",
"moo": "^0.5.0",
"railroad-diagrams": "^1.0.0",
"randexp": "0.4.6"
},
"bin": {
"nearley-railroad": "bin/nearley-railroad.js",
"nearley-test": "bin/nearley-test.js",
"nearley-unparse": "bin/nearley-unparse.js",
"nearleyc": "bin/nearleyc.js"
},
"funding": {
"type": "individual",
"url": "https://nearley.js.org/#give-to-nearley"
}
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -1134,6 +1312,183 @@
"node": ">=16.20.0" "node": ">=16.20.0"
} }
}, },
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/prismarine-biome": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/prismarine-biome/-/prismarine-biome-1.3.0.tgz",
"integrity": "sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==",
"license": "MIT",
"peerDependencies": {
"minecraft-data": "^3.0.0",
"prismarine-registry": "^1.1.0"
}
},
"node_modules/prismarine-block": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/prismarine-block/-/prismarine-block-1.22.0.tgz",
"integrity": "sha512-SKVTm+CHR5mY/d+delryqzB2hMH8HIPse8b0O51Ez2R5zPFtKVCLGk5NG71kCDKbMzhhev2iF0O4UC/fWDXhRw==",
"license": "MIT",
"dependencies": {
"minecraft-data": "^3.38.0",
"prismarine-biome": "^1.1.0",
"prismarine-chat": "^1.5.0",
"prismarine-item": "^1.10.1",
"prismarine-nbt": "^2.0.0",
"prismarine-registry": "^1.1.0"
}
},
"node_modules/prismarine-chat": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/prismarine-chat/-/prismarine-chat-1.12.0.tgz",
"integrity": "sha512-+1QBUn4WGXbAGwoGwJy31/FvH6JtTBHh//yU0xwOiVnBO71+6Ij0hYMd9PzTTAwR9bySfl/YLltGPBftUAOYOA==",
"license": "MIT",
"dependencies": {
"mojangson": "^2.0.1",
"prismarine-nbt": "^2.0.0",
"prismarine-registry": "^1.4.0"
}
},
"node_modules/prismarine-item": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/prismarine-item/-/prismarine-item-1.17.0.tgz",
"integrity": "sha512-wN1OjP+f+Uvtjo3KzeCkVSy96CqZ8yG7cvuvlGwcYupQ6ct7LtNkubHp0AHuLMJ0vbbfAC0oZ2bWOgI1DYp8WA==",
"license": "MIT",
"dependencies": {
"prismarine-nbt": "^2.0.0",
"prismarine-registry": "^1.4.0"
}
},
"node_modules/prismarine-nbt": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/prismarine-nbt/-/prismarine-nbt-2.8.0.tgz",
"integrity": "sha512-5D6FUZq0PNtf3v/41ImDlwThVesOv5adyqCRMZLzmkUGEmRJNNh5C6AsnvrClBftXs+IF0yqPnZoj8kcNPiMGg==",
"license": "MIT",
"dependencies": {
"protodef": "^1.18.0"
}
},
"node_modules/prismarine-registry": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/prismarine-registry/-/prismarine-registry-1.11.0.tgz",
"integrity": "sha512-uTvWE+bILxYv4i5MrrlxPQ0KYWINv1DJ3P2570GLC8uCdByDiDLBFfVyk4BrqOZBlDBft9CnaJMeOsC1Ly1iXw==",
"license": "MIT",
"dependencies": {
"minecraft-data": "^3.70.0",
"prismarine-block": "^1.17.1",
"prismarine-nbt": "^2.0.0"
}
},
"node_modules/prismarine-schematic": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/prismarine-schematic/-/prismarine-schematic-1.2.3.tgz",
"integrity": "sha512-Mwpn43vEHhm3aw3cPhJjWqztkW+nX+QLajDHlTask8lEOTGl1WmpvFja4iwiws4GIvaC8x0Foptf4uvDsnjrAg==",
"license": "MIT",
"dependencies": {
"minecraft-data": "^3.0.0",
"prismarine-block": "^1.7.2",
"prismarine-nbt": "^2.0.0",
"prismarine-world": "^3.1.1",
"vec3": "^0.1.7"
}
},
"node_modules/prismarine-world": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/prismarine-world/-/prismarine-world-3.6.3.tgz",
"integrity": "sha512-zqdqPEYCDHzqi6hglJldEO63bOROXpbZeIdxBmoQq7o04Lf81t016LU6stFHo3E+bmp5+xU74eDFdOvzYNABkA==",
"license": "MIT",
"dependencies": {
"vec3": "^0.1.7"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/protodef": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/protodef/-/protodef-1.19.0.tgz",
"integrity": "sha512-94f3GR7pk4Qi5YVLaLvWBfTGUIzzO8hyo7vFVICQuu5f5nwKtgGDaeC1uXIu49s5to/49QQhEYeL0aigu1jEGA==",
"license": "MIT",
"dependencies": {
"lodash.reduce": "^4.6.0",
"protodef-validator": "^1.3.0",
"readable-stream": "^4.4.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/protodef-validator": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/protodef-validator/-/protodef-validator-1.4.0.tgz",
"integrity": "sha512-2y2coBolqCEuk5Kc3QwO7ThR+/7TZiOit4FrpAgl+vFMvq8w76nDhh09z08e2NQOdrgPLsN2yzXsvRvtADgUZQ==",
"license": "MIT",
"dependencies": {
"ajv": "^6.5.4"
},
"bin": {
"protodef-validator": "cli.js"
}
},
"node_modules/protodef-validator/node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/protodef-validator/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"license": "MIT"
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1147,6 +1502,15 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.2", "version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
@@ -1162,6 +1526,25 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/railroad-diagrams": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
"integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==",
"license": "CC0-1.0"
},
"node_modules/randexp": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
"integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
"license": "MIT",
"dependencies": {
"discontinuous-range": "1.0.0",
"ret": "~0.1.10"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -1202,6 +1585,22 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/require-from-string": { "node_modules/require-from-string": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -1211,6 +1610,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ret": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"license": "MIT",
"engines": {
"node": ">=0.12"
}
},
"node_modules/router": { "node_modules/router": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -1439,6 +1847,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1470,6 +1887,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -1488,6 +1914,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/vec3": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==",
"license": "BSD"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -11,6 +11,9 @@
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1", "@modelcontextprotocol/sdk": "^1.27.1",
"express": "^4.21.2", "express": "^4.21.2",
"minecraft-data": "^3.105.0",
"playwright": "^1.58.2",
"prismarine-schematic": "^1.2.3",
"ws": "^8.18.0", "ws": "^8.18.0",
"zod": "^3.25.0" "zod": "^3.25.0"
}, },

View File

@@ -1,4 +1,5 @@
import { log } from './utils.js'; import { log } from './utils.js';
import { JAVA_TO_BEDROCK } from './java-block-ids.js';
const TAG = 'BlockMap'; const TAG = 'BlockMap';
@@ -615,6 +616,13 @@ export function resolveBlock(gcId, gcName) {
} }
} }
// Try Java string ID (for prismarine-schematic sources)
const javaId = gcId?.replace(/^minecraft:/, '');
if (javaId && JAVA_TO_BEDROCK.has(javaId)) {
const entry = JAVA_TO_BEDROCK.get(javaId);
return { block: entry.bedrock, data: entry.data, matched: true, name: entry.name };
}
// Try name-based lookup // Try name-based lookup
if (gcName) { if (gcName) {
const lower = gcName.toLowerCase().trim(); const lower = gcName.toLowerCase().trim();

View File

@@ -1,5 +1,6 @@
import { BedrockWebSocket } from './bedrock-ws.js'; import { BedrockWebSocket } from './bedrock-ws.js';
import { startMcpServer } from './mcp-server.js'; import { startMcpServer } from './mcp-server.js';
import { closeBrowser } from './schematics-browser.js';
import { log } from './utils.js'; import { log } from './utils.js';
const TAG = 'Main'; const TAG = 'Main';
@@ -26,8 +27,9 @@ log(TAG, 'In Minecraft, type: /connect ws://<your-ip>:' + WS_PORT);
log(TAG, ''); log(TAG, '');
// Graceful shutdown // Graceful shutdown
function shutdown(signal) { async function shutdown(signal) {
log(TAG, `${signal} received, shutting down...`); log(TAG, `${signal} received, shutting down...`);
await closeBrowser();
bedrock.stop(); bedrock.stop();
process.exit(0); process.exit(0);
} }

556
src/java-block-ids.js Normal file
View File

@@ -0,0 +1,556 @@
/**
* Maps modern Java Edition string IDs (minecraft:oak_planks style) to
* Bedrock Edition block IDs compatible with the existing block-map.js system.
*
* Used by prismarine-schematic parsed schematics where block.name is a
* Java string ID like "oak_planks" rather than a numeric "5:0" GrabCraft ID.
*/
export const JAVA_TO_BEDROCK = new Map([
// ── Air ──
['air', { bedrock: 'air', data: 0, name: 'Air' }],
['cave_air', { bedrock: 'air', data: 0, name: 'Air' }],
['void_air', { bedrock: 'air', data: 0, name: 'Air' }],
// ── Stone variants ──
['stone', { bedrock: 'stone', data: 0, name: 'Stone' }],
['granite', { bedrock: 'stone', data: 1, name: 'Granite' }],
['polished_granite', { bedrock: 'stone', data: 2, name: 'Polished Granite' }],
['diorite', { bedrock: 'stone', data: 3, name: 'Diorite' }],
['polished_diorite', { bedrock: 'stone', data: 4, name: 'Polished Diorite' }],
['andesite', { bedrock: 'stone', data: 5, name: 'Andesite' }],
['polished_andesite', { bedrock: 'stone', data: 6, name: 'Polished Andesite' }],
['deepslate', { bedrock: 'deepslate', data: 0, name: 'Deepslate' }],
['cobbled_deepslate', { bedrock: 'cobbled_deepslate', data: 0, name: 'Cobbled Deepslate' }],
['polished_deepslate', { bedrock: 'polished_deepslate', data: 0, name: 'Polished Deepslate' }],
['calcite', { bedrock: 'calcite', data: 0, name: 'Calcite' }],
['tuff', { bedrock: 'tuff', data: 0, name: 'Tuff' }],
['dripstone_block', { bedrock: 'dripstone_block', data: 0, name: 'Dripstone Block' }],
// ── Grass & Dirt ──
['grass_block', { bedrock: 'grass_block', data: 0, name: 'Grass Block' }],
['dirt', { bedrock: 'dirt', data: 0, name: 'Dirt' }],
['coarse_dirt', { bedrock: 'dirt', data: 1, name: 'Coarse Dirt' }],
['podzol', { bedrock: 'podzol', data: 0, name: 'Podzol' }],
['rooted_dirt', { bedrock: 'dirt_with_roots', data: 0, name: 'Rooted Dirt' }],
['mud', { bedrock: 'mud', data: 0, name: 'Mud' }],
['muddy_mangrove_roots', { bedrock: 'muddy_mangrove_roots', data: 0, name: 'Muddy Mangrove Roots' }],
// ── Cobblestone ──
['cobblestone', { bedrock: 'cobblestone', data: 0, name: 'Cobblestone' }],
['mossy_cobblestone', { bedrock: 'mossy_cobblestone', data: 0, name: 'Mossy Cobblestone' }],
// ── Planks ──
['oak_planks', { bedrock: 'planks', data: 0, name: 'Oak Planks' }],
['spruce_planks', { bedrock: 'planks', data: 1, name: 'Spruce Planks' }],
['birch_planks', { bedrock: 'planks', data: 2, name: 'Birch Planks' }],
['jungle_planks', { bedrock: 'planks', data: 3, name: 'Jungle Planks' }],
['acacia_planks', { bedrock: 'planks', data: 4, name: 'Acacia Planks' }],
['dark_oak_planks', { bedrock: 'planks', data: 5, name: 'Dark Oak Planks' }],
['crimson_planks', { bedrock: 'crimson_planks', data: 0, name: 'Crimson Planks' }],
['warped_planks', { bedrock: 'warped_planks', data: 0, name: 'Warped Planks' }],
['mangrove_planks', { bedrock: 'mangrove_planks', data: 0, name: 'Mangrove Planks' }],
['cherry_planks', { bedrock: 'cherry_planks', data: 0, name: 'Cherry Planks' }],
['bamboo_planks', { bedrock: 'bamboo_planks', data: 0, name: 'Bamboo Planks' }],
// ── Logs ──
['oak_log', { bedrock: 'log', data: 0, name: 'Oak Log' }],
['spruce_log', { bedrock: 'log', data: 1, name: 'Spruce Log' }],
['birch_log', { bedrock: 'log', data: 2, name: 'Birch Log' }],
['jungle_log', { bedrock: 'log', data: 3, name: 'Jungle Log' }],
['acacia_log', { bedrock: 'log2', data: 0, name: 'Acacia Log' }],
['dark_oak_log', { bedrock: 'log2', data: 1, name: 'Dark Oak Log' }],
['crimson_stem', { bedrock: 'crimson_stem', data: 0, name: 'Crimson Stem' }],
['warped_stem', { bedrock: 'warped_stem', data: 0, name: 'Warped Stem' }],
['mangrove_log', { bedrock: 'mangrove_log', data: 0, name: 'Mangrove Log' }],
['cherry_log', { bedrock: 'cherry_log', data: 0, name: 'Cherry Log' }],
['stripped_oak_log', { bedrock: 'stripped_oak_log', data: 0, name: 'Stripped Oak Log' }],
['stripped_spruce_log', { bedrock: 'stripped_spruce_log', data: 0, name: 'Stripped Spruce Log' }],
['stripped_birch_log', { bedrock: 'stripped_birch_log', data: 0, name: 'Stripped Birch Log' }],
['stripped_jungle_log', { bedrock: 'stripped_jungle_log', data: 0, name: 'Stripped Jungle Log' }],
['stripped_acacia_log', { bedrock: 'stripped_acacia_log', data: 0, name: 'Stripped Acacia Log' }],
['stripped_dark_oak_log', { bedrock: 'stripped_dark_oak_log', data: 0, name: 'Stripped Dark Oak Log' }],
// ── Wood (bark on all sides) ──
['oak_wood', { bedrock: 'wood', data: 0, name: 'Oak Wood' }],
['spruce_wood', { bedrock: 'wood', data: 1, name: 'Spruce Wood' }],
['birch_wood', { bedrock: 'wood', data: 2, name: 'Birch Wood' }],
['jungle_wood', { bedrock: 'wood', data: 3, name: 'Jungle Wood' }],
['acacia_wood', { bedrock: 'wood', data: 4, name: 'Acacia Wood' }],
['dark_oak_wood', { bedrock: 'wood', data: 5, name: 'Dark Oak Wood' }],
// ── Leaves ──
['oak_leaves', { bedrock: 'leaves', data: 0, name: 'Oak Leaves' }],
['spruce_leaves', { bedrock: 'leaves', data: 1, name: 'Spruce Leaves' }],
['birch_leaves', { bedrock: 'leaves', data: 2, name: 'Birch Leaves' }],
['jungle_leaves', { bedrock: 'leaves', data: 3, name: 'Jungle Leaves' }],
['acacia_leaves', { bedrock: 'leaves2', data: 0, name: 'Acacia Leaves' }],
['dark_oak_leaves', { bedrock: 'leaves2', data: 1, name: 'Dark Oak Leaves' }],
['azalea_leaves', { bedrock: 'azalea_leaves', data: 0, name: 'Azalea Leaves' }],
['mangrove_leaves', { bedrock: 'mangrove_leaves', data: 0, name: 'Mangrove Leaves' }],
['cherry_leaves', { bedrock: 'cherry_leaves', data: 0, name: 'Cherry Leaves' }],
// ── Sand & Gravel ──
['sand', { bedrock: 'sand', data: 0, name: 'Sand' }],
['red_sand', { bedrock: 'sand', data: 1, name: 'Red Sand' }],
['gravel', { bedrock: 'gravel', data: 0, name: 'Gravel' }],
// ── Ores ──
['coal_ore', { bedrock: 'coal_ore', data: 0, name: 'Coal Ore' }],
['iron_ore', { bedrock: 'iron_ore', data: 0, name: 'Iron Ore' }],
['gold_ore', { bedrock: 'gold_ore', data: 0, name: 'Gold Ore' }],
['diamond_ore', { bedrock: 'diamond_ore', data: 0, name: 'Diamond Ore' }],
['emerald_ore', { bedrock: 'emerald_ore', data: 0, name: 'Emerald Ore' }],
['lapis_ore', { bedrock: 'lapis_ore', data: 0, name: 'Lapis Lazuli Ore' }],
['redstone_ore', { bedrock: 'redstone_ore', data: 0, name: 'Redstone Ore' }],
['copper_ore', { bedrock: 'copper_ore', data: 0, name: 'Copper Ore' }],
['deepslate_coal_ore', { bedrock: 'deepslate_coal_ore', data: 0, name: 'Deepslate Coal Ore' }],
['deepslate_iron_ore', { bedrock: 'deepslate_iron_ore', data: 0, name: 'Deepslate Iron Ore' }],
['deepslate_gold_ore', { bedrock: 'deepslate_gold_ore', data: 0, name: 'Deepslate Gold Ore' }],
['deepslate_diamond_ore', { bedrock: 'deepslate_diamond_ore', data: 0, name: 'Deepslate Diamond Ore' }],
['deepslate_emerald_ore', { bedrock: 'deepslate_emerald_ore', data: 0, name: 'Deepslate Emerald Ore' }],
['deepslate_lapis_ore', { bedrock: 'deepslate_lapis_ore', data: 0, name: 'Deepslate Lapis Ore' }],
['deepslate_redstone_ore', { bedrock: 'deepslate_redstone_ore', data: 0, name: 'Deepslate Redstone Ore' }],
['deepslate_copper_ore', { bedrock: 'deepslate_copper_ore', data: 0, name: 'Deepslate Copper Ore' }],
['nether_gold_ore', { bedrock: 'nether_gold_ore', data: 0, name: 'Nether Gold Ore' }],
['nether_quartz_ore', { bedrock: 'quartz_ore', data: 0, name: 'Nether Quartz Ore' }],
['ancient_debris', { bedrock: 'ancient_debris', data: 0, name: 'Ancient Debris' }],
// ── Mineral blocks ──
['coal_block', { bedrock: 'coal_block', data: 0, name: 'Block of Coal' }],
['iron_block', { bedrock: 'iron_block', data: 0, name: 'Block of Iron' }],
['gold_block', { bedrock: 'gold_block', data: 0, name: 'Block of Gold' }],
['diamond_block', { bedrock: 'diamond_block', data: 0, name: 'Block of Diamond' }],
['emerald_block', { bedrock: 'emerald_block', data: 0, name: 'Block of Emerald' }],
['lapis_block', { bedrock: 'lapis_block', data: 0, name: 'Lapis Lazuli Block' }],
['redstone_block', { bedrock: 'redstone_block', data: 0, name: 'Block of Redstone' }],
['netherite_block', { bedrock: 'netherite_block', data: 0, name: 'Netherite Block' }],
['copper_block', { bedrock: 'copper_block', data: 0, name: 'Copper Block' }],
['raw_iron_block', { bedrock: 'raw_iron_block', data: 0, name: 'Raw Iron Block' }],
['raw_gold_block', { bedrock: 'raw_gold_block', data: 0, name: 'Raw Gold Block' }],
['raw_copper_block', { bedrock: 'raw_copper_block', data: 0, name: 'Raw Copper Block' }],
['amethyst_block', { bedrock: 'amethyst_block', data: 0, name: 'Amethyst Block' }],
// ── Sandstone ──
['sandstone', { bedrock: 'sandstone', data: 0, name: 'Sandstone' }],
['chiseled_sandstone', { bedrock: 'sandstone', data: 1, name: 'Chiseled Sandstone' }],
['cut_sandstone', { bedrock: 'sandstone', data: 2, name: 'Cut Sandstone' }],
['smooth_sandstone', { bedrock: 'sandstone', data: 3, name: 'Smooth Sandstone' }],
['red_sandstone', { bedrock: 'red_sandstone', data: 0, name: 'Red Sandstone' }],
['chiseled_red_sandstone', { bedrock: 'red_sandstone', data: 1, name: 'Chiseled Red Sandstone' }],
['cut_red_sandstone', { bedrock: 'red_sandstone', data: 2, name: 'Cut Red Sandstone' }],
['smooth_red_sandstone', { bedrock: 'red_sandstone', data: 3, name: 'Smooth Red Sandstone' }],
// ── Wool ──
['white_wool', { bedrock: 'wool', data: 0, name: 'White Wool' }],
['orange_wool', { bedrock: 'wool', data: 1, name: 'Orange Wool' }],
['magenta_wool', { bedrock: 'wool', data: 2, name: 'Magenta Wool' }],
['light_blue_wool', { bedrock: 'wool', data: 3, name: 'Light Blue Wool' }],
['yellow_wool', { bedrock: 'wool', data: 4, name: 'Yellow Wool' }],
['lime_wool', { bedrock: 'wool', data: 5, name: 'Lime Wool' }],
['pink_wool', { bedrock: 'wool', data: 6, name: 'Pink Wool' }],
['gray_wool', { bedrock: 'wool', data: 7, name: 'Gray Wool' }],
['light_gray_wool', { bedrock: 'wool', data: 8, name: 'Light Gray Wool' }],
['cyan_wool', { bedrock: 'wool', data: 9, name: 'Cyan Wool' }],
['purple_wool', { bedrock: 'wool', data: 10, name: 'Purple Wool' }],
['blue_wool', { bedrock: 'wool', data: 11, name: 'Blue Wool' }],
['brown_wool', { bedrock: 'wool', data: 12, name: 'Brown Wool' }],
['green_wool', { bedrock: 'wool', data: 13, name: 'Green Wool' }],
['red_wool', { bedrock: 'wool', data: 14, name: 'Red Wool' }],
['black_wool', { bedrock: 'wool', data: 15, name: 'Black Wool' }],
// ── Glass ──
['glass', { bedrock: 'glass', data: 0, name: 'Glass' }],
['glass_pane', { bedrock: 'glass_pane', data: 0, name: 'Glass Pane' }],
['tinted_glass', { bedrock: 'tinted_glass', data: 0, name: 'Tinted Glass' }],
['white_stained_glass', { bedrock: 'stained_glass', data: 0, name: 'White Stained Glass' }],
['orange_stained_glass', { bedrock: 'stained_glass', data: 1, name: 'Orange Stained Glass' }],
['magenta_stained_glass', { bedrock: 'stained_glass', data: 2, name: 'Magenta Stained Glass' }],
['light_blue_stained_glass', { bedrock: 'stained_glass', data: 3, name: 'Light Blue Stained Glass' }],
['yellow_stained_glass', { bedrock: 'stained_glass', data: 4, name: 'Yellow Stained Glass' }],
['lime_stained_glass', { bedrock: 'stained_glass', data: 5, name: 'Lime Stained Glass' }],
['pink_stained_glass', { bedrock: 'stained_glass', data: 6, name: 'Pink Stained Glass' }],
['gray_stained_glass', { bedrock: 'stained_glass', data: 7, name: 'Gray Stained Glass' }],
['light_gray_stained_glass', { bedrock: 'stained_glass', data: 8, name: 'Light Gray Stained Glass' }],
['cyan_stained_glass', { bedrock: 'stained_glass', data: 9, name: 'Cyan Stained Glass' }],
['purple_stained_glass', { bedrock: 'stained_glass', data: 10, name: 'Purple Stained Glass' }],
['blue_stained_glass', { bedrock: 'stained_glass', data: 11, name: 'Blue Stained Glass' }],
['brown_stained_glass', { bedrock: 'stained_glass', data: 12, name: 'Brown Stained Glass' }],
['green_stained_glass', { bedrock: 'stained_glass', data: 13, name: 'Green Stained Glass' }],
['red_stained_glass', { bedrock: 'stained_glass', data: 14, name: 'Red Stained Glass' }],
['black_stained_glass', { bedrock: 'stained_glass', data: 15, name: 'Black Stained Glass' }],
['white_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 0, name: 'White Stained Glass Pane' }],
['orange_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 1, name: 'Orange Stained Glass Pane' }],
['magenta_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 2, name: 'Magenta Stained Glass Pane' }],
['light_blue_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 3, name: 'Light Blue Stained Glass Pane' }],
['yellow_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 4, name: 'Yellow Stained Glass Pane' }],
['lime_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 5, name: 'Lime Stained Glass Pane' }],
['pink_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 6, name: 'Pink Stained Glass Pane' }],
['gray_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 7, name: 'Gray Stained Glass Pane' }],
['light_gray_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 8, name: 'Light Gray Stained Glass Pane' }],
['cyan_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 9, name: 'Cyan Stained Glass Pane' }],
['purple_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 10, name: 'Purple Stained Glass Pane' }],
['blue_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 11, name: 'Blue Stained Glass Pane' }],
['brown_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 12, name: 'Brown Stained Glass Pane' }],
['green_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 13, name: 'Green Stained Glass Pane' }],
['red_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 14, name: 'Red Stained Glass Pane' }],
['black_stained_glass_pane', { bedrock: 'stained_glass_pane', data: 15, name: 'Black Stained Glass Pane' }],
// ── Bricks ──
['bricks', { bedrock: 'brick_block', data: 0, name: 'Bricks' }],
['stone_bricks', { bedrock: 'stonebrick', data: 0, name: 'Stone Bricks' }],
['mossy_stone_bricks', { bedrock: 'stonebrick', data: 1, name: 'Mossy Stone Bricks' }],
['cracked_stone_bricks', { bedrock: 'stonebrick', data: 2, name: 'Cracked Stone Bricks' }],
['chiseled_stone_bricks', { bedrock: 'stonebrick', data: 3, name: 'Chiseled Stone Bricks' }],
['nether_bricks', { bedrock: 'nether_brick', data: 0, name: 'Nether Bricks' }],
['red_nether_bricks', { bedrock: 'red_nether_brick', data: 0, name: 'Red Nether Bricks' }],
['deepslate_bricks', { bedrock: 'deepslate_bricks', data: 0, name: 'Deepslate Bricks' }],
['deepslate_tiles', { bedrock: 'deepslate_tiles', data: 0, name: 'Deepslate Tiles' }],
['mud_bricks', { bedrock: 'mud_bricks', data: 0, name: 'Mud Bricks' }],
// ── Slabs ──
['oak_slab', { bedrock: 'wooden_slab', data: 0, name: 'Oak Slab' }],
['spruce_slab', { bedrock: 'wooden_slab', data: 1, name: 'Spruce Slab' }],
['birch_slab', { bedrock: 'wooden_slab', data: 2, name: 'Birch Slab' }],
['jungle_slab', { bedrock: 'wooden_slab', data: 3, name: 'Jungle Slab' }],
['acacia_slab', { bedrock: 'wooden_slab', data: 4, name: 'Acacia Slab' }],
['dark_oak_slab', { bedrock: 'wooden_slab', data: 5, name: 'Dark Oak Slab' }],
['stone_slab', { bedrock: 'stone_block_slab', data: 0, name: 'Stone Slab' }],
['sandstone_slab', { bedrock: 'stone_block_slab', data: 1, name: 'Sandstone Slab' }],
['cobblestone_slab', { bedrock: 'stone_block_slab', data: 3, name: 'Cobblestone Slab' }],
['brick_slab', { bedrock: 'stone_block_slab', data: 4, name: 'Brick Slab' }],
['stone_brick_slab', { bedrock: 'stone_block_slab', data: 5, name: 'Stone Brick Slab' }],
['smooth_stone_slab', { bedrock: 'stone_block_slab', data: 0, name: 'Smooth Stone Slab' }],
// ── Stairs ──
['oak_stairs', { bedrock: 'oak_stairs', data: 0, name: 'Oak Stairs' }],
['spruce_stairs', { bedrock: 'spruce_stairs', data: 0, name: 'Spruce Stairs' }],
['birch_stairs', { bedrock: 'birch_stairs', data: 0, name: 'Birch Stairs' }],
['jungle_stairs', { bedrock: 'jungle_stairs', data: 0, name: 'Jungle Stairs' }],
['acacia_stairs', { bedrock: 'acacia_stairs', data: 0, name: 'Acacia Stairs' }],
['dark_oak_stairs', { bedrock: 'dark_oak_stairs', data: 0, name: 'Dark Oak Stairs' }],
['cobblestone_stairs', { bedrock: 'stone_stairs', data: 0, name: 'Cobblestone Stairs' }],
['stone_stairs', { bedrock: 'normal_stone_stairs', data: 0, name: 'Stone Stairs' }],
['stone_brick_stairs', { bedrock: 'stone_brick_stairs', data: 0, name: 'Stone Brick Stairs' }],
['brick_stairs', { bedrock: 'brick_stairs', data: 0, name: 'Brick Stairs' }],
['sandstone_stairs', { bedrock: 'sandstone_stairs', data: 0, name: 'Sandstone Stairs' }],
['red_sandstone_stairs', { bedrock: 'red_sandstone_stairs', data: 0, name: 'Red Sandstone Stairs' }],
['nether_brick_stairs', { bedrock: 'nether_brick_stairs', data: 0, name: 'Nether Brick Stairs' }],
['quartz_stairs', { bedrock: 'quartz_stairs', data: 0, name: 'Quartz Stairs' }],
['purpur_stairs', { bedrock: 'purpur_stairs', data: 0, name: 'Purpur Stairs' }],
['prismarine_stairs', { bedrock: 'prismarine_stairs', data: 0, name: 'Prismarine Stairs' }],
['crimson_stairs', { bedrock: 'crimson_stairs', data: 0, name: 'Crimson Stairs' }],
['warped_stairs', { bedrock: 'warped_stairs', data: 0, name: 'Warped Stairs' }],
// ── Fences ──
['oak_fence', { bedrock: 'fence', data: 0, name: 'Oak Fence' }],
['spruce_fence', { bedrock: 'fence', data: 1, name: 'Spruce Fence' }],
['birch_fence', { bedrock: 'fence', data: 2, name: 'Birch Fence' }],
['jungle_fence', { bedrock: 'fence', data: 3, name: 'Jungle Fence' }],
['acacia_fence', { bedrock: 'fence', data: 4, name: 'Acacia Fence' }],
['dark_oak_fence', { bedrock: 'fence', data: 5, name: 'Dark Oak Fence' }],
['nether_brick_fence', { bedrock: 'nether_brick_fence', data: 0, name: 'Nether Brick Fence' }],
['crimson_fence', { bedrock: 'crimson_fence', data: 0, name: 'Crimson Fence' }],
['warped_fence', { bedrock: 'warped_fence', data: 0, name: 'Warped Fence' }],
// ── Fence Gates ──
['oak_fence_gate', { bedrock: 'fence_gate', data: 0, name: 'Oak Fence Gate' }],
['spruce_fence_gate', { bedrock: 'spruce_fence_gate', data: 0, name: 'Spruce Fence Gate' }],
['birch_fence_gate', { bedrock: 'birch_fence_gate', data: 0, name: 'Birch Fence Gate' }],
['jungle_fence_gate', { bedrock: 'jungle_fence_gate', data: 0, name: 'Jungle Fence Gate' }],
['acacia_fence_gate', { bedrock: 'acacia_fence_gate', data: 0, name: 'Acacia Fence Gate' }],
['dark_oak_fence_gate', { bedrock: 'dark_oak_fence_gate', data: 0, name: 'Dark Oak Fence Gate' }],
// ── Doors ──
['oak_door', { bedrock: 'wooden_door', data: 0, name: 'Oak Door' }],
['spruce_door', { bedrock: 'spruce_door', data: 0, name: 'Spruce Door' }],
['birch_door', { bedrock: 'birch_door', data: 0, name: 'Birch Door' }],
['jungle_door', { bedrock: 'jungle_door', data: 0, name: 'Jungle Door' }],
['acacia_door', { bedrock: 'acacia_door', data: 0, name: 'Acacia Door' }],
['dark_oak_door', { bedrock: 'dark_oak_door', data: 0, name: 'Dark Oak Door' }],
['iron_door', { bedrock: 'iron_door', data: 0, name: 'Iron Door' }],
['crimson_door', { bedrock: 'crimson_door', data: 0, name: 'Crimson Door' }],
['warped_door', { bedrock: 'warped_door', data: 0, name: 'Warped Door' }],
// ── Trapdoors ──
['oak_trapdoor', { bedrock: 'trapdoor', data: 0, name: 'Oak Trapdoor' }],
['iron_trapdoor', { bedrock: 'iron_trapdoor', data: 0, name: 'Iron Trapdoor' }],
['crimson_trapdoor', { bedrock: 'crimson_trapdoor', data: 0, name: 'Crimson Trapdoor' }],
['warped_trapdoor', { bedrock: 'warped_trapdoor', data: 0, name: 'Warped Trapdoor' }],
// ── Walls ──
['cobblestone_wall', { bedrock: 'cobblestone_wall', data: 0, name: 'Cobblestone Wall' }],
['mossy_cobblestone_wall', { bedrock: 'cobblestone_wall', data: 1, name: 'Mossy Cobblestone Wall' }],
['stone_brick_wall', { bedrock: 'cobblestone_wall', data: 6, name: 'Stone Brick Wall' }],
['brick_wall', { bedrock: 'cobblestone_wall', data: 5, name: 'Brick Wall' }],
['nether_brick_wall', { bedrock: 'cobblestone_wall', data: 9, name: 'Nether Brick Wall' }],
// ── Concrete ──
['white_concrete', { bedrock: 'concrete', data: 0, name: 'White Concrete' }],
['orange_concrete', { bedrock: 'concrete', data: 1, name: 'Orange Concrete' }],
['magenta_concrete', { bedrock: 'concrete', data: 2, name: 'Magenta Concrete' }],
['light_blue_concrete', { bedrock: 'concrete', data: 3, name: 'Light Blue Concrete' }],
['yellow_concrete', { bedrock: 'concrete', data: 4, name: 'Yellow Concrete' }],
['lime_concrete', { bedrock: 'concrete', data: 5, name: 'Lime Concrete' }],
['pink_concrete', { bedrock: 'concrete', data: 6, name: 'Pink Concrete' }],
['gray_concrete', { bedrock: 'concrete', data: 7, name: 'Gray Concrete' }],
['light_gray_concrete', { bedrock: 'concrete', data: 8, name: 'Light Gray Concrete' }],
['cyan_concrete', { bedrock: 'concrete', data: 9, name: 'Cyan Concrete' }],
['purple_concrete', { bedrock: 'concrete', data: 10, name: 'Purple Concrete' }],
['blue_concrete', { bedrock: 'concrete', data: 11, name: 'Blue Concrete' }],
['brown_concrete', { bedrock: 'concrete', data: 12, name: 'Brown Concrete' }],
['green_concrete', { bedrock: 'concrete', data: 13, name: 'Green Concrete' }],
['red_concrete', { bedrock: 'concrete', data: 14, name: 'Red Concrete' }],
['black_concrete', { bedrock: 'concrete', data: 15, name: 'Black Concrete' }],
// ── Terracotta ──
['terracotta', { bedrock: 'hardened_clay', data: 0, name: 'Terracotta' }],
['white_terracotta', { bedrock: 'stained_hardened_clay', data: 0, name: 'White Terracotta' }],
['orange_terracotta', { bedrock: 'stained_hardened_clay', data: 1, name: 'Orange Terracotta' }],
['magenta_terracotta', { bedrock: 'stained_hardened_clay', data: 2, name: 'Magenta Terracotta' }],
['light_blue_terracotta', { bedrock: 'stained_hardened_clay', data: 3, name: 'Light Blue Terracotta' }],
['yellow_terracotta', { bedrock: 'stained_hardened_clay', data: 4, name: 'Yellow Terracotta' }],
['lime_terracotta', { bedrock: 'stained_hardened_clay', data: 5, name: 'Lime Terracotta' }],
['pink_terracotta', { bedrock: 'stained_hardened_clay', data: 6, name: 'Pink Terracotta' }],
['gray_terracotta', { bedrock: 'stained_hardened_clay', data: 7, name: 'Gray Terracotta' }],
['light_gray_terracotta', { bedrock: 'stained_hardened_clay', data: 8, name: 'Light Gray Terracotta' }],
['cyan_terracotta', { bedrock: 'stained_hardened_clay', data: 9, name: 'Cyan Terracotta' }],
['purple_terracotta', { bedrock: 'stained_hardened_clay', data: 10, name: 'Purple Terracotta' }],
['blue_terracotta', { bedrock: 'stained_hardened_clay', data: 11, name: 'Blue Terracotta' }],
['brown_terracotta', { bedrock: 'stained_hardened_clay', data: 12, name: 'Brown Terracotta' }],
['green_terracotta', { bedrock: 'stained_hardened_clay', data: 13, name: 'Green Terracotta' }],
['red_terracotta', { bedrock: 'stained_hardened_clay', data: 14, name: 'Red Terracotta' }],
['black_terracotta', { bedrock: 'stained_hardened_clay', data: 15, name: 'Black Terracotta' }],
// ── Glazed Terracotta ──
['white_glazed_terracotta', { bedrock: 'white_glazed_terracotta', data: 0, name: 'White Glazed Terracotta' }],
['orange_glazed_terracotta', { bedrock: 'orange_glazed_terracotta', data: 0, name: 'Orange Glazed Terracotta' }],
['magenta_glazed_terracotta', { bedrock: 'magenta_glazed_terracotta', data: 0, name: 'Magenta Glazed Terracotta' }],
['light_blue_glazed_terracotta', { bedrock: 'light_blue_glazed_terracotta', data: 0, name: 'Light Blue Glazed Terracotta' }],
['yellow_glazed_terracotta', { bedrock: 'yellow_glazed_terracotta', data: 0, name: 'Yellow Glazed Terracotta' }],
['lime_glazed_terracotta', { bedrock: 'lime_glazed_terracotta', data: 0, name: 'Lime Glazed Terracotta' }],
['pink_glazed_terracotta', { bedrock: 'pink_glazed_terracotta', data: 0, name: 'Pink Glazed Terracotta' }],
['gray_glazed_terracotta', { bedrock: 'gray_glazed_terracotta', data: 0, name: 'Gray Glazed Terracotta' }],
['light_gray_glazed_terracotta', { bedrock: 'silver_glazed_terracotta', data: 0, name: 'Light Gray Glazed Terracotta' }],
['cyan_glazed_terracotta', { bedrock: 'cyan_glazed_terracotta', data: 0, name: 'Cyan Glazed Terracotta' }],
['purple_glazed_terracotta', { bedrock: 'purple_glazed_terracotta', data: 0, name: 'Purple Glazed Terracotta' }],
['blue_glazed_terracotta', { bedrock: 'blue_glazed_terracotta', data: 0, name: 'Blue Glazed Terracotta' }],
['brown_glazed_terracotta', { bedrock: 'brown_glazed_terracotta', data: 0, name: 'Brown Glazed Terracotta' }],
['green_glazed_terracotta', { bedrock: 'green_glazed_terracotta', data: 0, name: 'Green Glazed Terracotta' }],
['red_glazed_terracotta', { bedrock: 'red_glazed_terracotta', data: 0, name: 'Red Glazed Terracotta' }],
['black_glazed_terracotta', { bedrock: 'black_glazed_terracotta', data: 0, name: 'Black Glazed Terracotta' }],
// ── Carpet ──
['white_carpet', { bedrock: 'carpet', data: 0, name: 'White Carpet' }],
['orange_carpet', { bedrock: 'carpet', data: 1, name: 'Orange Carpet' }],
['magenta_carpet', { bedrock: 'carpet', data: 2, name: 'Magenta Carpet' }],
['light_blue_carpet', { bedrock: 'carpet', data: 3, name: 'Light Blue Carpet' }],
['yellow_carpet', { bedrock: 'carpet', data: 4, name: 'Yellow Carpet' }],
['lime_carpet', { bedrock: 'carpet', data: 5, name: 'Lime Carpet' }],
['pink_carpet', { bedrock: 'carpet', data: 6, name: 'Pink Carpet' }],
['gray_carpet', { bedrock: 'carpet', data: 7, name: 'Gray Carpet' }],
['light_gray_carpet', { bedrock: 'carpet', data: 8, name: 'Light Gray Carpet' }],
['cyan_carpet', { bedrock: 'carpet', data: 9, name: 'Cyan Carpet' }],
['purple_carpet', { bedrock: 'carpet', data: 10, name: 'Purple Carpet' }],
['blue_carpet', { bedrock: 'carpet', data: 11, name: 'Blue Carpet' }],
['brown_carpet', { bedrock: 'carpet', data: 12, name: 'Brown Carpet' }],
['green_carpet', { bedrock: 'carpet', data: 13, name: 'Green Carpet' }],
['red_carpet', { bedrock: 'carpet', data: 14, name: 'Red Carpet' }],
['black_carpet', { bedrock: 'carpet', data: 15, name: 'Black Carpet' }],
// ── Quartz ──
['quartz_block', { bedrock: 'quartz_block', data: 0, name: 'Quartz Block' }],
['chiseled_quartz_block', { bedrock: 'quartz_block', data: 1, name: 'Chiseled Quartz' }],
['quartz_pillar', { bedrock: 'quartz_block', data: 2, name: 'Pillar Quartz' }],
['smooth_quartz', { bedrock: 'quartz_block', data: 3, name: 'Smooth Quartz' }],
// ── Prismarine ──
['prismarine', { bedrock: 'prismarine', data: 0, name: 'Prismarine' }],
['prismarine_bricks', { bedrock: 'prismarine', data: 1, name: 'Prismarine Bricks' }],
['dark_prismarine', { bedrock: 'prismarine', data: 2, name: 'Dark Prismarine' }],
['sea_lantern', { bedrock: 'sea_lantern', data: 0, name: 'Sea Lantern' }],
// ── Purpur ──
['purpur_block', { bedrock: 'purpur_block', data: 0, name: 'Purpur Block' }],
['purpur_pillar', { bedrock: 'purpur_pillar', data: 0, name: 'Purpur Pillar' }],
// ── End Stone ──
['end_stone', { bedrock: 'end_stone', data: 0, name: 'End Stone' }],
['end_stone_bricks', { bedrock: 'end_bricks', data: 0, name: 'End Stone Bricks' }],
// ── Nether blocks ──
['netherrack', { bedrock: 'netherrack', data: 0, name: 'Netherrack' }],
['soul_sand', { bedrock: 'soul_sand', data: 0, name: 'Soul Sand' }],
['soul_soil', { bedrock: 'soul_soil', data: 0, name: 'Soul Soil' }],
['glowstone', { bedrock: 'glowstone', data: 0, name: 'Glowstone' }],
['nether_wart_block', { bedrock: 'nether_wart_block', data: 0, name: 'Nether Wart Block' }],
['warped_wart_block', { bedrock: 'warped_wart_block', data: 0, name: 'Warped Wart Block' }],
['basalt', { bedrock: 'basalt', data: 0, name: 'Basalt' }],
['polished_basalt', { bedrock: 'polished_basalt', data: 0, name: 'Polished Basalt' }],
['smooth_basalt', { bedrock: 'smooth_basalt', data: 0, name: 'Smooth Basalt' }],
['blackstone', { bedrock: 'blackstone', data: 0, name: 'Blackstone' }],
['polished_blackstone', { bedrock: 'polished_blackstone', data: 0, name: 'Polished Blackstone' }],
['polished_blackstone_bricks', { bedrock: 'polished_blackstone_bricks', data: 0, name: 'Polished Blackstone Bricks' }],
['crying_obsidian', { bedrock: 'crying_obsidian', data: 0, name: 'Crying Obsidian' }],
['shroomlight', { bedrock: 'shroomlight', data: 0, name: 'Shroomlight' }],
['crimson_nylium', { bedrock: 'crimson_nylium', data: 0, name: 'Crimson Nylium' }],
['warped_nylium', { bedrock: 'warped_nylium', data: 0, name: 'Warped Nylium' }],
['magma_block', { bedrock: 'magma', data: 0, name: 'Magma Block' }],
// ── Misc utility blocks ──
['bedrock', { bedrock: 'bedrock', data: 0, name: 'Bedrock' }],
['obsidian', { bedrock: 'obsidian', data: 0, name: 'Obsidian' }],
['water', { bedrock: 'water', data: 0, name: 'Water' }],
['lava', { bedrock: 'lava', data: 0, name: 'Lava' }],
['ice', { bedrock: 'ice', data: 0, name: 'Ice' }],
['packed_ice', { bedrock: 'packed_ice', data: 0, name: 'Packed Ice' }],
['blue_ice', { bedrock: 'blue_ice', data: 0, name: 'Blue Ice' }],
['snow_block', { bedrock: 'snow', data: 0, name: 'Snow Block' }],
['snow', { bedrock: 'snow_layer', data: 0, name: 'Snow Layer' }],
['clay', { bedrock: 'clay', data: 0, name: 'Clay' }],
['sponge', { bedrock: 'sponge', data: 0, name: 'Sponge' }],
['wet_sponge', { bedrock: 'sponge', data: 1, name: 'Wet Sponge' }],
['tnt', { bedrock: 'tnt', data: 0, name: 'TNT' }],
['bookshelf', { bedrock: 'bookshelf', data: 0, name: 'Bookshelf' }],
['torch', { bedrock: 'torch', data: 0, name: 'Torch' }],
['wall_torch', { bedrock: 'torch', data: 0, name: 'Wall Torch' }],
['soul_torch', { bedrock: 'soul_torch', data: 0, name: 'Soul Torch' }],
['lantern', { bedrock: 'lantern', data: 0, name: 'Lantern' }],
['soul_lantern', { bedrock: 'soul_lantern', data: 0, name: 'Soul Lantern' }],
['chest', { bedrock: 'chest', data: 0, name: 'Chest' }],
['crafting_table', { bedrock: 'crafting_table', data: 0, name: 'Crafting Table' }],
['furnace', { bedrock: 'furnace', data: 0, name: 'Furnace' }],
['blast_furnace', { bedrock: 'blast_furnace', data: 0, name: 'Blast Furnace' }],
['smoker', { bedrock: 'smoker', data: 0, name: 'Smoker' }],
['barrel', { bedrock: 'barrel', data: 0, name: 'Barrel' }],
['ladder', { bedrock: 'ladder', data: 0, name: 'Ladder' }],
['iron_bars', { bedrock: 'iron_bars', data: 0, name: 'Iron Bars' }],
['chain', { bedrock: 'chain', data: 0, name: 'Chain' }],
['hay_block', { bedrock: 'hay_block', data: 0, name: 'Hay Bale' }],
['slime_block', { bedrock: 'slime', data: 0, name: 'Slime Block' }],
['honey_block', { bedrock: 'honey_block', data: 0, name: 'Honey Block' }],
['honeycomb_block', { bedrock: 'honeycomb_block', data: 0, name: 'Honeycomb Block' }],
['bone_block', { bedrock: 'bone_block', data: 0, name: 'Bone Block' }],
['cactus', { bedrock: 'cactus', data: 0, name: 'Cactus' }],
['pumpkin', { bedrock: 'pumpkin', data: 0, name: 'Pumpkin' }],
['carved_pumpkin', { bedrock: 'carved_pumpkin', data: 0, name: 'Carved Pumpkin' }],
['jack_o_lantern', { bedrock: 'lit_pumpkin', data: 0, name: "Jack o'Lantern" }],
['melon', { bedrock: 'melon_block', data: 0, name: 'Melon Block' }],
['anvil', { bedrock: 'anvil', data: 0, name: 'Anvil' }],
['bell', { bedrock: 'bell', data: 0, name: 'Bell' }],
['jukebox', { bedrock: 'jukebox', data: 0, name: 'Jukebox' }],
['enchanting_table', { bedrock: 'enchanting_table', data: 0, name: 'Enchanting Table' }],
['brewing_stand', { bedrock: 'brewing_stand', data: 0, name: 'Brewing Stand' }],
['cauldron', { bedrock: 'cauldron', data: 0, name: 'Cauldron' }],
['beacon', { bedrock: 'beacon', data: 0, name: 'Beacon' }],
['ender_chest', { bedrock: 'ender_chest', data: 0, name: 'Ender Chest' }],
['end_portal_frame', { bedrock: 'end_portal_frame', data: 0, name: 'End Portal Frame' }],
['end_rod', { bedrock: 'end_rod', data: 0, name: 'End Rod' }],
['flower_pot', { bedrock: 'flower_pot', data: 0, name: 'Flower Pot' }],
['barrier', { bedrock: 'barrier', data: 0, name: 'Barrier' }],
['mycelium', { bedrock: 'mycelium', data: 0, name: 'Mycelium' }],
['lily_pad', { bedrock: 'waterlily', data: 0, name: 'Lily Pad' }],
['vine', { bedrock: 'vine', data: 0, name: 'Vines' }],
['cobweb', { bedrock: 'web', data: 0, name: 'Cobweb' }],
['moss_block', { bedrock: 'moss_block', data: 0, name: 'Moss Block' }],
['sculk', { bedrock: 'sculk', data: 0, name: 'Sculk' }],
['scaffolding', { bedrock: 'scaffolding', data: 0, name: 'Scaffolding' }],
['lodestone', { bedrock: 'lodestone', data: 0, name: 'Lodestone' }],
['respawn_anchor', { bedrock: 'respawn_anchor', data: 0, name: 'Respawn Anchor' }],
['observer', { bedrock: 'observer', data: 0, name: 'Observer' }],
['dispenser', { bedrock: 'dispenser', data: 0, name: 'Dispenser' }],
['dropper', { bedrock: 'dropper', data: 0, name: 'Dropper' }],
['hopper', { bedrock: 'hopper', data: 0, name: 'Hopper' }],
['note_block', { bedrock: 'noteblock', data: 0, name: 'Note Block' }],
['trapped_chest', { bedrock: 'trapped_chest', data: 0, name: 'Trapped Chest' }],
['shulker_box', { bedrock: 'shulker_box', data: 0, name: 'Shulker Box' }],
['dragon_egg', { bedrock: 'dragon_egg', data: 0, name: 'Dragon Egg' }],
// ── Redstone ──
['redstone_wire', { bedrock: 'redstone_wire', data: 0, name: 'Redstone Wire' }],
['redstone_torch', { bedrock: 'redstone_torch', data: 0, name: 'Redstone Torch' }],
['redstone_lamp', { bedrock: 'redstone_lamp', data: 0, name: 'Redstone Lamp' }],
['lever', { bedrock: 'lever', data: 0, name: 'Lever' }],
['stone_button', { bedrock: 'stone_button', data: 0, name: 'Stone Button' }],
['oak_button', { bedrock: 'wooden_button', data: 0, name: 'Oak Button' }],
['stone_pressure_plate', { bedrock: 'stone_pressure_plate', data: 0, name: 'Stone Pressure Plate' }],
['oak_pressure_plate', { bedrock: 'wooden_pressure_plate', data: 0, name: 'Oak Pressure Plate' }],
['piston', { bedrock: 'piston', data: 0, name: 'Piston' }],
['sticky_piston', { bedrock: 'sticky_piston', data: 0, name: 'Sticky Piston' }],
['repeater', { bedrock: 'unpowered_repeater', data: 0, name: 'Repeater' }],
['comparator', { bedrock: 'unpowered_comparator', data: 0, name: 'Comparator' }],
['daylight_detector', { bedrock: 'daylight_detector', data: 0, name: 'Daylight Detector' }],
['tripwire_hook', { bedrock: 'tripwire_hook', data: 0, name: 'Tripwire Hook' }],
['target', { bedrock: 'target', data: 0, name: 'Target' }],
// ── Rails ──
['rail', { bedrock: 'rail', data: 0, name: 'Rail' }],
['powered_rail', { bedrock: 'golden_rail', data: 0, name: 'Powered Rail' }],
['detector_rail', { bedrock: 'detector_rail', data: 0, name: 'Detector Rail' }],
['activator_rail', { bedrock: 'activator_rail', data: 0, name: 'Activator Rail' }],
// ── Flowers & Plants ──
['dandelion', { bedrock: 'yellow_flower', data: 0, name: 'Dandelion' }],
['poppy', { bedrock: 'red_flower', data: 0, name: 'Poppy' }],
['blue_orchid', { bedrock: 'red_flower', data: 1, name: 'Blue Orchid' }],
['allium', { bedrock: 'red_flower', data: 2, name: 'Allium' }],
['azure_bluet', { bedrock: 'red_flower', data: 3, name: 'Azure Bluet' }],
['red_tulip', { bedrock: 'red_flower', data: 4, name: 'Red Tulip' }],
['orange_tulip', { bedrock: 'red_flower', data: 5, name: 'Orange Tulip' }],
['white_tulip', { bedrock: 'red_flower', data: 6, name: 'White Tulip' }],
['pink_tulip', { bedrock: 'red_flower', data: 7, name: 'Pink Tulip' }],
['oxeye_daisy', { bedrock: 'red_flower', data: 8, name: 'Oxeye Daisy' }],
['sunflower', { bedrock: 'double_plant', data: 0, name: 'Sunflower' }],
['lilac', { bedrock: 'double_plant', data: 1, name: 'Lilac' }],
['tall_grass', { bedrock: 'double_plant', data: 2, name: 'Double Tallgrass' }],
['large_fern', { bedrock: 'double_plant', data: 3, name: 'Large Fern' }],
['rose_bush', { bedrock: 'double_plant', data: 4, name: 'Rose Bush' }],
['peony', { bedrock: 'double_plant', data: 5, name: 'Peony' }],
['short_grass', { bedrock: 'tallgrass', data: 1, name: 'Grass' }],
['fern', { bedrock: 'tallgrass', data: 2, name: 'Fern' }],
['dead_bush', { bedrock: 'deadbush', data: 0, name: 'Dead Bush' }],
['brown_mushroom', { bedrock: 'brown_mushroom', data: 0, name: 'Brown Mushroom' }],
['red_mushroom', { bedrock: 'red_mushroom', data: 0, name: 'Red Mushroom' }],
['sugar_cane', { bedrock: 'reeds', data: 0, name: 'Sugar Cane' }],
['nether_wart', { bedrock: 'nether_wart', data: 0, name: 'Nether Wart' }],
['chorus_plant', { bedrock: 'chorus_plant', data: 0, name: 'Chorus Plant' }],
['chorus_flower', { bedrock: 'chorus_flower', data: 0, name: 'Chorus Flower' }],
['bamboo', { bedrock: 'bamboo', data: 0, name: 'Bamboo' }],
['kelp', { bedrock: 'kelp', data: 0, name: 'Kelp' }],
['sea_pickle', { bedrock: 'sea_pickle', data: 0, name: 'Sea Pickle' }],
// ── Signs ──
['oak_sign', { bedrock: 'standing_sign', data: 0, name: 'Sign' }],
['oak_wall_sign', { bedrock: 'wall_sign', data: 0, name: 'Wall Sign' }],
// ── Beds ──
['white_bed', { bedrock: 'bed', data: 0, name: 'Bed' }],
['red_bed', { bedrock: 'bed', data: 14, name: 'Red Bed' }],
// ── Banners ──
['white_banner', { bedrock: 'standing_banner', data: 0, name: 'Banner' }],
// ── Copper blocks ──
['waxed_copper_block', { bedrock: 'waxed_copper', data: 0, name: 'Waxed Copper Block' }],
['exposed_copper', { bedrock: 'exposed_copper', data: 0, name: 'Exposed Copper' }],
['weathered_copper', { bedrock: 'weathered_copper', data: 0, name: 'Weathered Copper' }],
['oxidized_copper', { bedrock: 'oxidized_copper', data: 0, name: 'Oxidized Copper' }],
['cut_copper', { bedrock: 'cut_copper', data: 0, name: 'Cut Copper' }],
['lightning_rod', { bedrock: 'lightning_rod', data: 0, name: 'Lightning Rod' }],
// ── Misc modern blocks ──
['smooth_stone', { bedrock: 'smooth_stone', data: 0, name: 'Smooth Stone' }],
['dried_kelp_block', { bedrock: 'dried_kelp_block', data: 0, name: 'Dried Kelp Block' }],
['campfire', { bedrock: 'campfire', data: 0, name: 'Campfire' }],
['soul_campfire', { bedrock: 'soul_campfire', data: 0, name: 'Soul Campfire' }],
['grindstone', { bedrock: 'grindstone', data: 0, name: 'Grindstone' }],
['stonecutter', { bedrock: 'stonecutter_block', data: 0, name: 'Stonecutter' }],
['composter', { bedrock: 'composter', data: 0, name: 'Composter' }],
['lectern', { bedrock: 'lectern', data: 0, name: 'Lectern' }],
['cartography_table', { bedrock: 'cartography_table', data: 0, name: 'Cartography Table' }],
['fletching_table', { bedrock: 'fletching_table', data: 0, name: 'Fletching Table' }],
['smithing_table', { bedrock: 'smithing_table', data: 0, name: 'Smithing Table' }],
['loom', { bedrock: 'loom', data: 0, name: 'Loom' }],
['beehive', { bedrock: 'beehive', data: 0, name: 'Beehive' }],
['bee_nest', { bedrock: 'bee_nest', data: 0, name: 'Bee Nest' }],
]);

View File

@@ -6,6 +6,7 @@ import express from 'express';
import { z } from 'zod'; import { z } from 'zod';
import { log, logError, createCommandMessage } from './utils.js'; import { log, logError, createCommandMessage } from './utils.js';
import { searchBlueprints, fetchBlueprint, blueprintToCommands, getCategories } from './grabcraft.js'; import { searchBlueprints, fetchBlueprint, blueprintToCommands, getCategories } from './grabcraft.js';
import { searchSchematics, fetchSchematic, getSchematicCategories } from './schematics.js';
import { getAllBlocks } from './block-map.js'; import { getAllBlocks } from './block-map.js';
import { SHAPES } from './building-helpers.js'; import { SHAPES } from './building-helpers.js';
@@ -760,6 +761,188 @@ export function startMcpServer(bedrock, port = 3002) {
} }
); );
// ── Tool: minecraft_search_schematics (minecraft-schematics.com) ──
server.registerTool(
'minecraft_search_schematics',
{
title: 'Search Minecraft Schematics',
description:
'Search minecraft-schematics.com for downloadable schematics (20,000+ library). Returns names, URLs, and IDs. Use the URL with minecraft_build_schematic to construct the building. Requires Playwright browser automation.',
inputSchema: z.object({
query: z.string().describe('Search query, e.g. "castle", "medieval house", "modern city"'),
page: z.number().int().min(1).optional().describe('Page number (default 1)'),
}),
},
async ({ query, page }) => {
try {
const results = await searchSchematics(query, page ?? 1);
if (results.results.length === 0) {
return {
content: [
{
type: 'text',
text: `No schematics found for "${query}". Try a different search term.`,
},
],
};
}
const formatted = results.results
.map((r, i) => `${i + 1}. ${r.name}\n ID: ${r.id}${r.author ? ` | Author: ${r.author}` : ''}${r.category ? ` | Category: ${r.category}` : ''}${r.downloads ? ` | Downloads: ${r.downloads}` : ''}\n URL: ${r.url}`)
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Found ${results.total} schematics (page ${results.page}):\n\n${formatted}`,
},
],
};
} catch (err) {
return {
content: [{ type: 'text', text: `Error searching schematics: ${err.message}` }],
isError: true,
};
}
}
);
// ── Tool: minecraft_build_schematic (minecraft-schematics.com) ──
server.registerTool(
'minecraft_build_schematic',
{
title: 'Build Schematic',
description:
'Download a schematic from minecraft-schematics.com, parse it, and build it in Minecraft. If no coordinates given, builds at the player\'s current position. Use dryRun to preview materials and dimensions without building. Requires Playwright browser automation.',
inputSchema: z.object({
url: z.string().describe('Schematic URL from minecraft-schematics.com'),
x: z.number().int().optional().describe('Build origin X (default: player position)'),
y: z.number().int().optional().describe('Build origin Y (default: player position)'),
z: z.number().int().optional().describe('Build origin Z (default: player position)'),
dryRun: z.boolean().optional().describe('If true, returns material list and dimensions without building'),
}),
},
async ({ url, x, y, z: zCoord, dryRun }, { sendNotification }) => {
try {
// Determine build origin
let originX = x, originY = y, originZ = zCoord;
if (originX === undefined || originY === undefined || originZ === undefined) {
try {
const pos = await bedrock.getPlayerPosition();
originX = originX ?? pos.x;
originY = originY ?? pos.y;
originZ = originZ ?? pos.z;
} catch {
return {
content: [
{
type: 'text',
text: 'Error: Could not get player position. Specify x, y, z coordinates manually or ensure Minecraft is connected.',
},
],
isError: true,
};
}
}
// Fetch and parse schematic
const blueprint = await fetchSchematic(url);
if (blueprint.voxels.length === 0) {
return {
content: [
{
type: 'text',
text: `Schematic "${blueprint.name}" has no block data. The file may be empty or in an unsupported format.`,
},
],
isError: true,
};
}
// Convert to commands (reuses the same blueprintToCommands from grabcraft)
const { commands, summary } = blueprintToCommands(blueprint, originX, originY, originZ);
if (dryRun) {
const materialLines = Object.entries(summary.materials)
.sort((a, b) => b[1] - a[1])
.map(([name, count]) => ` ${name}: ${count}`)
.join('\n');
let text = `Schematic: ${summary.name}\n`;
text += `Dimensions: ${summary.dimensions.width}W x ${summary.dimensions.height}H x ${summary.dimensions.depth}D\n`;
text += `Total blocks: ${summary.totalCommands}\n`;
text += `Build origin: ${summary.buildOrigin.x}, ${summary.buildOrigin.y}, ${summary.buildOrigin.z}\n\n`;
text += `Materials:\n${materialLines}`;
if (summary.unmappedBlocks) {
text += `\n\nUnmapped blocks (using stone fallback):\n`;
text += Object.entries(summary.unmappedBlocks)
.map(([k, v]) => ` ${k}: ${v}`)
.join('\n');
}
return { content: [{ type: 'text', text }] };
}
// Check command limit
if (commands.length > bedrock.commandQueue.maxBuildCommands) {
return {
content: [
{
type: 'text',
text: `Schematic has ${commands.length} commands, exceeding the limit of ${bedrock.commandQueue.maxBuildCommands}. Use dryRun to preview, or increase MAX_BUILD_COMMANDS.`,
},
],
isError: true,
};
}
// Build it
const prepared = commands.map((line) => createCommandMessage(line));
const progressFn = (progress) => {
try {
sendNotification({
method: 'notifications/message',
params: {
level: 'info',
logger: 'minecraft-schematic',
data: `Building "${summary.name}": ${progress.percent}% (${progress.completed}/${progress.total})`,
},
});
} catch {
// Non-critical
}
};
const result = await bedrock.commandQueue.enqueueBatchWithProgress(prepared, progressFn);
let text = `Schematic "${summary.name}" build ${result.cancelled ? 'cancelled' : 'complete'}!\n`;
text += `Blocks placed: ${result.succeeded}/${result.total}\n`;
text += `Failed: ${result.failed}\n`;
text += `Dimensions: ${summary.dimensions.width}W x ${summary.dimensions.height}H x ${summary.dimensions.depth}D\n`;
text += `Build origin: ${summary.buildOrigin.x}, ${summary.buildOrigin.y}, ${summary.buildOrigin.z}`;
if (summary.unmappedBlocks) {
text += `\n\nUnmapped blocks (used stone): ${Object.keys(summary.unmappedBlocks).join(', ')}`;
}
return { content: [{ type: 'text', text }] };
} catch (err) {
return {
content: [{ type: 'text', text: `Error building schematic: ${err.message}` }],
isError: true,
};
}
}
);
// ── MCP Resources (Phase 6) ─────────────────────────────────────────
// Resource: GrabCraft categories // Resource: GrabCraft categories
server.resource( server.resource(
'grabcraft-categories', 'grabcraft-categories',
@@ -782,6 +965,28 @@ export function startMcpServer(bedrock, port = 3002) {
} }
); );
// Resource: minecraft-schematics.com categories
server.resource(
'schematics-categories',
'schematics://categories',
{
description: 'Available minecraft-schematics.com categories for searching',
mimeType: 'application/json',
},
async () => {
const categories = getSchematicCategories();
return {
contents: [
{
uri: 'schematics://categories',
mimeType: 'application/json',
text: JSON.stringify(categories, null, 2),
},
],
};
}
);
return server; return server;
} }

125
src/schematics-browser.js Normal file
View File

@@ -0,0 +1,125 @@
import { log, logError } from './utils.js';
const TAG = 'SchematicBrowser';
let browserInstance = null;
let browserPromise = null;
/**
* Get or launch a headless Chromium browser via Playwright.
* Reuses a single instance across the process lifetime.
*/
async function getBrowser() {
if (browserInstance) return browserInstance;
if (browserPromise) return browserPromise;
browserPromise = (async () => {
let pw;
try {
pw = await import('playwright');
} catch (err) {
throw new Error(
'Playwright is not installed. Run: npm install playwright && npx playwright install chromium'
);
}
log(TAG, 'Launching headless Chromium...');
browserInstance = await pw.chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
browserInstance.on('disconnected', () => {
log(TAG, 'Browser disconnected');
browserInstance = null;
browserPromise = null;
});
log(TAG, 'Browser ready');
return browserInstance;
})();
return browserPromise;
}
/**
* Fetch a page's HTML content via Playwright.
* @param {string} url
* @param {number} [timeoutMs=30000]
* @returns {Promise<string>} HTML content
*/
export async function fetchPage(url, timeoutMs = 30000) {
const browser = await getBrowser();
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
return await page.content();
} finally {
await page.close();
}
}
/**
* Download a file by navigating to a page and clicking a download link/button.
* @param {string} pageUrl - The page containing the download link
* @param {string} selector - CSS selector for the download link/button
* @param {number} [timeoutMs=60000]
* @returns {Promise<Buffer>} Downloaded file contents
*/
export async function downloadFile(pageUrl, selector, timeoutMs = 60000) {
const browser = await getBrowser();
const page = await browser.newPage();
try {
await page.goto(pageUrl, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: timeoutMs }),
page.click(selector),
]);
const path = await download.path();
if (!path) throw new Error('Download failed — no file path returned');
const { readFileSync } = await import('node:fs');
return readFileSync(path);
} finally {
await page.close();
}
}
/**
* Download a file directly by URL (no click required).
* @param {string} url - Direct download URL
* @param {number} [timeoutMs=60000]
* @returns {Promise<Buffer>}
*/
export async function downloadUrl(url, timeoutMs = 60000) {
const browser = await getBrowser();
const context = browser.contexts()[0] || await browser.newContext();
const page = await context.newPage();
try {
const response = await page.goto(url, { waitUntil: 'commit', timeout: timeoutMs });
if (!response || !response.ok()) {
throw new Error(`Download failed: HTTP ${response?.status()} for ${url}`);
}
return await response.body();
} finally {
await page.close();
}
}
/**
* Close the browser instance. Call during shutdown.
*/
export async function closeBrowser() {
if (browserInstance) {
log(TAG, 'Closing browser...');
try {
await browserInstance.close();
} catch {
// Already closed
}
browserInstance = null;
browserPromise = null;
}
}

104
src/schematics-cache.js Normal file
View File

@@ -0,0 +1,104 @@
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { log } from './utils.js';
const TAG = 'SchematicCache';
const BASE_DIR = './cache/schematics';
const LAYERS = ['search', 'meta', 'raw', 'parsed'];
const TTL = {
search: 24 * 60 * 60 * 1000, // 24 hours
meta: 7 * 24 * 60 * 60 * 1000, // 7 days
raw: 0, // permanent
parsed: 0, // permanent
};
let initialized = false;
function ensureDirs() {
if (initialized) return;
for (const layer of LAYERS) {
mkdirSync(join(BASE_DIR, layer), { recursive: true });
}
initialized = true;
}
/**
* Create a safe filename from a cache key.
*/
export function cacheKey(str) {
return createHash('sha256').update(str).digest('hex').slice(0, 16);
}
/**
* Get a JSON value from cache.
* @param {string} layer - Cache layer (search, meta, parsed)
* @param {string} key - Cache key
* @returns {any|null}
*/
export function get(layer, key) {
ensureDirs();
const path = join(BASE_DIR, layer, `${key}.json`);
try {
const raw = readFileSync(path, 'utf8');
const entry = JSON.parse(raw);
const ttl = TTL[layer];
if (ttl > 0 && Date.now() - entry.timestamp > ttl) {
unlinkSync(path);
return null;
}
return entry.data;
} catch {
return null;
}
}
/**
* Set a JSON value in cache.
* @param {string} layer - Cache layer
* @param {string} key - Cache key
* @param {any} data - Data to store
*/
export function set(layer, key, data) {
ensureDirs();
const path = join(BASE_DIR, layer, `${key}.json`);
try {
writeFileSync(path, JSON.stringify({ timestamp: Date.now(), data }));
} catch (err) {
log(TAG, `Cache write error (${layer}/${key}): ${err.message}`);
}
}
/**
* Get a binary buffer from cache.
* @param {string} layer - Cache layer (raw)
* @param {string} key - Cache key
* @returns {Buffer|null}
*/
export function getBuffer(layer, key) {
ensureDirs();
const path = join(BASE_DIR, layer, `${key}.schematic`);
try {
return readFileSync(path);
} catch {
return null;
}
}
/**
* Set a binary buffer in cache.
* @param {string} layer - Cache layer (raw)
* @param {string} key - Cache key
* @param {Buffer} buf - Buffer to store
*/
export function setBuffer(layer, key, buf) {
ensureDirs();
const path = join(BASE_DIR, layer, `${key}.schematic`);
try {
writeFileSync(path, buf);
} catch (err) {
log(TAG, `Cache write error (${layer}/${key}): ${err.message}`);
}
}

276
src/schematics.js Normal file
View File

@@ -0,0 +1,276 @@
import { log, logError } from './utils.js';
import { resolveBlock, formatBlock, getUnknownBlocks, clearUnknownBlocks } from './block-map.js';
import * as cache from './schematics-cache.js';
import { fetchPage, downloadUrl } from './schematics-browser.js';
const TAG = 'Schematics';
const BASE_URL = 'https://www.minecraft-schematics.com';
/**
* Search minecraft-schematics.com for schematics.
* @param {string} query
* @param {number} [page=1]
* @returns {Promise<{ results: Array<{ id: string, name: string, url: string, author: string, category: string, downloads: string }>, total: number, page: number }>}
*/
export async function searchSchematics(query, page = 1) {
const cacheId = cache.cacheKey(`${query}:${page}`);
const cached = cache.get('search', cacheId);
if (cached) {
log(TAG, `Search cache hit: "${query}" page ${page}`);
return cached;
}
const searchUrl = `${BASE_URL}/search/?q=${encodeURIComponent(query)}&page=${page}`;
log(TAG, `Searching: ${searchUrl}`);
let html;
try {
html = await fetchPage(searchUrl);
} catch (err) {
throw new Error(`Failed to search minecraft-schematics.com: ${err.message}`);
}
const results = [];
// Parse search results from the HTML
// Results are typically in list items or cards with links to /schematic/{id}/
const itemRegex = /<a[^>]+href="(\/schematic\/(\d+)\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
let match;
while ((match = itemRegex.exec(html)) !== null) {
const url = BASE_URL + match[1];
const id = match[2];
const inner = match[3];
// Extract the name from inner content
const nameText = inner.replace(/<[^>]+>/g, '').trim();
if (!nameText || nameText.length < 2) continue;
// Skip navigation/pagination links
if (/^\d+$/.test(nameText) || nameText === 'Next' || nameText === 'Previous') continue;
// Avoid duplicate IDs
if (results.some(r => r.id === id)) continue;
results.push({
id,
name: nameText.slice(0, 100),
url,
author: '',
category: '',
downloads: '',
});
}
// Try to extract additional metadata from surrounding HTML
// Look for author, category, download count near each result
for (const result of results) {
const idPattern = new RegExp(`schematic/${result.id}/[\\s\\S]{0,2000}`, 'i');
const context = html.match(idPattern);
if (context) {
const ctx = context[0];
const authorMatch = ctx.match(/(?:by|author)[:\s]*([^<\n]+)/i);
if (authorMatch) result.author = authorMatch[1].trim().slice(0, 50);
const catMatch = ctx.match(/category[:\s]*([^<\n]+)/i);
if (catMatch) result.category = catMatch[1].trim().slice(0, 50);
const dlMatch = ctx.match(/([\d,]+)\s*download/i);
if (dlMatch) result.downloads = dlMatch[1];
}
}
const totalMatch = html.match(/([\d,]+)\s*(?:results?|schematics?)\s*found/i);
const total = totalMatch ? parseInt(totalMatch[1].replace(/,/g, ''), 10) : results.length;
const result = { results, total, page };
cache.set('search', cacheId, result);
log(TAG, `Found ${results.length} results (total: ${total})`);
return result;
}
/**
* Fetch and parse a schematic from minecraft-schematics.com.
* @param {string} url - URL like https://www.minecraft-schematics.com/schematic/24287/
* @returns {Promise<object>} Blueprint-compatible object with voxels array
*/
export async function fetchSchematic(url) {
// Extract ID from URL
const idMatch = url.match(/schematic\/(\d+)/);
if (!idMatch) throw new Error(`Invalid schematic URL: ${url}`);
const id = idMatch[1];
// Check parsed cache
const parsedData = cache.get('parsed', id);
if (parsedData) {
log(TAG, `Parsed cache hit: ${id}`);
return parsedData;
}
// Check raw cache for already-downloaded schematic file
let rawBuffer = cache.getBuffer('raw', id);
if (!rawBuffer) {
// Fetch the schematic page to find the download link
log(TAG, `Fetching schematic page: ${url}`);
let html;
try {
html = await fetchPage(url);
} catch (err) {
throw new Error(`Failed to fetch schematic page: ${err.message}`);
}
// Extract metadata
const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
const name = titleMatch
? titleMatch[1].replace(/\s*[-|].*Minecraft Schematics.*/i, '').trim()
: `Schematic ${id}`;
// Cache metadata
cache.set('meta', id, { name, url });
// Find download URL — look for the download link/button
// minecraft-schematics.com uses /schematic/{id}/download/ or similar
const downloadUrlPath = `/schematic/${id}/download/`;
const fullDownloadUrl = BASE_URL + downloadUrlPath;
log(TAG, `Downloading schematic file: ${fullDownloadUrl}`);
try {
rawBuffer = await downloadUrl(fullDownloadUrl);
} catch (err) {
// Try alternate download approach — look for direct link in page
const dlMatch = html.match(/href="([^"]*download[^"]*)"/i);
if (dlMatch) {
const altUrl = dlMatch[1].startsWith('http') ? dlMatch[1] : BASE_URL + dlMatch[1];
log(TAG, `Trying alternate download URL: ${altUrl}`);
rawBuffer = await downloadUrl(altUrl);
} else {
throw new Error(`Failed to download schematic: ${err.message}`);
}
}
if (!rawBuffer || rawBuffer.length === 0) {
throw new Error('Downloaded schematic file is empty');
}
cache.setBuffer('raw', id, rawBuffer);
log(TAG, `Cached raw schematic: ${rawBuffer.length} bytes`);
}
// Parse with prismarine-schematic
const blueprint = await parseSchematicBuffer(rawBuffer, id, url);
cache.set('parsed', id, blueprint);
return blueprint;
}
/**
* Parse a raw .schematic/.schem buffer into our blueprint format.
* @param {Buffer} buffer
* @param {string} id
* @param {string} url
* @returns {Promise<object>}
*/
async function parseSchematicBuffer(buffer, id, url) {
let Schematic, Vec3;
try {
const mod = await import('prismarine-schematic');
Schematic = mod.Schematic || mod.default?.Schematic;
const vec3Mod = await import('vec3');
Vec3 = vec3Mod.Vec3 || vec3Mod.default;
} catch (err) {
throw new Error(`prismarine-schematic not available: ${err.message}`);
}
let schematic;
try {
schematic = await Schematic.read(buffer);
} catch (err) {
throw new Error(`Failed to parse schematic file: ${err.message}. The file may be in an unsupported format.`);
}
// Get name from cached metadata
const meta = cache.get('meta', id);
const name = meta?.name || `Schematic ${id}`;
// Extract voxels by iterating all blocks
const voxels = [];
const start = schematic.start();
const end = schematic.end();
for (let y = start.y; y <= end.y; y++) {
for (let z = start.z; z <= end.z; z++) {
for (let x = start.x; x <= end.x; x++) {
const pos = new Vec3(x, y, z);
const block = schematic.getBlock(pos);
if (!block || block.name === 'air' || block.name === 'cave_air' || block.name === 'void_air') {
continue;
}
voxels.push({
x: x - start.x,
y: y - start.y,
z: z - start.z,
matId: block.name,
matName: block.name,
hex: null,
});
}
}
}
const size = schematic.size;
const dimensions = {
width: size.x,
height: size.y,
depth: size.z,
};
const blueprint = {
name,
url,
voxels,
materials: [],
dimensions,
totalBlocks: voxels.length,
origin: { x: 0, y: 0, z: 0 },
};
log(TAG, `Parsed "${name}": ${voxels.length} blocks, ${dimensions.width}x${dimensions.height}x${dimensions.depth}`);
return blueprint;
}
/**
* Convert a schematic blueprint to Bedrock setblock commands.
* Re-exports blueprintToCommands from grabcraft.js for convenience,
* but works identically since voxels use the same format.
*/
export { blueprintToCommands } from './grabcraft.js';
/**
* Get categories for minecraft-schematics.com.
*/
export function getSchematicCategories() {
return [
{ name: 'Castle', slug: 'castle' },
{ name: 'Medieval', slug: 'medieval' },
{ name: 'House', slug: 'house' },
{ name: 'Modern', slug: 'modern' },
{ name: 'Tower', slug: 'tower' },
{ name: 'Ship', slug: 'ship' },
{ name: 'Church', slug: 'church' },
{ name: 'Temple', slug: 'temple' },
{ name: 'Bridge', slug: 'bridge' },
{ name: 'Statue', slug: 'statue' },
{ name: 'Farm', slug: 'farm' },
{ name: 'Redstone', slug: 'redstone' },
{ name: 'Pixel Art', slug: 'pixel-art' },
{ name: 'Survival', slug: 'survival' },
{ name: 'Fantasy', slug: 'fantasy' },
{ name: 'Sci-Fi', slug: 'sci-fi' },
{ name: 'Vehicle', slug: 'vehicle' },
{ name: 'Nature', slug: 'nature' },
{ name: 'Underground', slug: 'underground' },
{ name: 'Other', slug: 'other' },
];
}