feat(schematics): add schematic search/build with Playwright browser support
Some checks failed
Deploy to Docker / deploy (push) Failing after 33s
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
cache/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22-alpine
|
||||
FROM mcr.microsoft.com/playwright/javascript:v1.58.2-noble
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -7,8 +7,11 @@ RUN npm ci --production 2>/dev/null || npm install --production
|
||||
|
||||
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
|
||||
|
||||
USER node
|
||||
USER pwuser
|
||||
|
||||
CMD ["node", "src/index.js"]
|
||||
|
||||
@@ -9,4 +9,9 @@ services:
|
||||
- WS_PORT=3001
|
||||
- MCP_PORT=3002
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
- schematic-cache:/app/cache
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
schematic-cache:
|
||||
|
||||
432
package-lock.json
generated
432
package-lock.json
generated
@@ -10,6 +10,9 @@
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"express": "^4.21.2",
|
||||
"minecraft-data": "^3.105.0",
|
||||
"playwright": "^1.58.2",
|
||||
"prismarine-schematic": "^1.2.3",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25.0"
|
||||
},
|
||||
@@ -354,6 +357,18 @@
|
||||
"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": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -406,6 +421,26 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"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": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
@@ -445,6 +480,30 @@
|
||||
"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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -483,6 +542,12 @@
|
||||
"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": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -578,6 +643,12 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -652,6 +723,24 @@
|
||||
"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": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||
@@ -743,6 +832,12 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
@@ -795,6 +890,20 @@
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -918,6 +1027,26 @@
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -975,6 +1104,12 @@
|
||||
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -1044,12 +1179,55 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"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": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@@ -1134,6 +1312,183 @@
|
||||
"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": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -1147,6 +1502,15 @@
|
||||
"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": {
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
@@ -1162,6 +1526,25 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -1202,6 +1585,22 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@@ -1211,6 +1610,15 @@
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
@@ -1439,6 +1847,15 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1470,6 +1887,15 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@@ -1488,6 +1914,12 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"express": "^4.21.2",
|
||||
"minecraft-data": "^3.105.0",
|
||||
"playwright": "^1.58.2",
|
||||
"prismarine-schematic": "^1.2.3",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25.0"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { log } from './utils.js';
|
||||
import { JAVA_TO_BEDROCK } from './java-block-ids.js';
|
||||
|
||||
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
|
||||
if (gcName) {
|
||||
const lower = gcName.toLowerCase().trim();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BedrockWebSocket } from './bedrock-ws.js';
|
||||
import { startMcpServer } from './mcp-server.js';
|
||||
import { closeBrowser } from './schematics-browser.js';
|
||||
import { log } from './utils.js';
|
||||
|
||||
const TAG = 'Main';
|
||||
@@ -26,8 +27,9 @@ log(TAG, 'In Minecraft, type: /connect ws://<your-ip>:' + WS_PORT);
|
||||
log(TAG, '');
|
||||
|
||||
// Graceful shutdown
|
||||
function shutdown(signal) {
|
||||
async function shutdown(signal) {
|
||||
log(TAG, `${signal} received, shutting down...`);
|
||||
await closeBrowser();
|
||||
bedrock.stop();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
556
src/java-block-ids.js
Normal file
556
src/java-block-ids.js
Normal 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' }],
|
||||
]);
|
||||
@@ -6,6 +6,7 @@ import express from 'express';
|
||||
import { z } from 'zod';
|
||||
import { log, logError, createCommandMessage } from './utils.js';
|
||||
import { searchBlueprints, fetchBlueprint, blueprintToCommands, getCategories } from './grabcraft.js';
|
||||
import { searchSchematics, fetchSchematic, getSchematicCategories } from './schematics.js';
|
||||
import { getAllBlocks } from './block-map.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
|
||||
server.resource(
|
||||
'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;
|
||||
}
|
||||
|
||||
|
||||
125
src/schematics-browser.js
Normal file
125
src/schematics-browser.js
Normal 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
104
src/schematics-cache.js
Normal 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
276
src/schematics.js
Normal 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' },
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user