From 2e91bcf63dd6de3ee2609d74642d0b4e57316708 Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Tue, 17 Mar 2026 00:07:49 +0000 Subject: [PATCH] feat(schematics): add schematic search/build with Playwright browser support 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) --- .gitignore | 1 + Dockerfile | 7 +- docker-compose.yml | 5 + package-lock.json | 432 +++++++++++++++++++++++++++++ package.json | 3 + src/block-map.js | 8 + src/index.js | 4 +- src/java-block-ids.js | 556 ++++++++++++++++++++++++++++++++++++++ src/mcp-server.js | 205 ++++++++++++++ src/schematics-browser.js | 125 +++++++++ src/schematics-cache.js | 104 +++++++ src/schematics.js | 276 +++++++++++++++++++ 12 files changed, 1723 insertions(+), 3 deletions(-) create mode 100644 src/java-block-ids.js create mode 100644 src/schematics-browser.js create mode 100644 src/schematics-cache.js create mode 100644 src/schematics.js diff --git a/.gitignore b/.gitignore index ca6d62c..3b4b634 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ .env *.log .DS_Store +cache/ diff --git a/Dockerfile b/Dockerfile index f40572b..605b85a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/docker-compose.yml b/docker-compose.yml index 9293c9c..e170c5a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/package-lock.json b/package-lock.json index 5a878a2..e5da633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8fd032d..affd998 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/block-map.js b/src/block-map.js index 96fb43b..ef56d9a 100644 --- a/src/block-map.js +++ b/src/block-map.js @@ -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(); diff --git a/src/index.js b/src/index.js index 0e3f616..28c3b49 100644 --- a/src/index.js +++ b/src/index.js @@ -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://:' + 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); } diff --git a/src/java-block-ids.js b/src/java-block-ids.js new file mode 100644 index 0000000..dc2af92 --- /dev/null +++ b/src/java-block-ids.js @@ -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' }], +]); diff --git a/src/mcp-server.js b/src/mcp-server.js index 13174f5..4d00f70 100644 --- a/src/mcp-server.js +++ b/src/mcp-server.js @@ -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; } diff --git a/src/schematics-browser.js b/src/schematics-browser.js new file mode 100644 index 0000000..7d12684 --- /dev/null +++ b/src/schematics-browser.js @@ -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} 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} 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} + */ +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; + } +} diff --git a/src/schematics-cache.js b/src/schematics-cache.js new file mode 100644 index 0000000..c679206 --- /dev/null +++ b/src/schematics-cache.js @@ -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}`); + } +} diff --git a/src/schematics.js b/src/schematics.js new file mode 100644 index 0000000..332c150 --- /dev/null +++ b/src/schematics.js @@ -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 = /]+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} 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>/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' }, + ]; +}