diff --git a/.github/workflows/build-zip.yml b/.github/workflows/build-zip.yml new file mode 100644 index 000000000..cc4724768 --- /dev/null +++ b/.github/workflows/build-zip.yml @@ -0,0 +1,43 @@ +name: Make Self Host Zip + +on: + workflow_dispatch: + +jobs: + build-and-bundle: + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Checkout repository + uses: actions/checkout@master + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install + + - name: Build project + run: pnpm build + + - name: Bundle server.js + run: | + pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'" + + - name: Create distribution package + run: | + mkdir -p package + cp -r dist package/ + cp bundled-server.js package/server.js + cd package + zip -r ../self-host.zip . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: self-host + path: self-host.zip diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc54da4e6..92b7e7f3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - run: pnpm build-storybook - run: pnpm test-unit - run: pnpm lint - - run: pnpm tsx scripts/buildNpmReact.ts + # - run: pnpm tsx scripts/buildNpmReact.ts - run: nohup pnpm prod-start & - run: nohup pnpm test-mc-server & - uses: cypress-io/github-action@v5 diff --git a/.github/workflows/next-deploy.yml b/.github/workflows/next-deploy.yml index 665abb30b..042302a41 100644 --- a/.github/workflows/next-deploy.yml +++ b/.github/workflows/next-deploy.yml @@ -32,6 +32,8 @@ jobs: echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\", \"isCommit\": true}" > assets/release.json - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + env: + CONFIG_JSON_SOURCE: BUNDLED - run: pnpm build-storybook - name: Copy playground files run: | diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 18c80e8c7..6408c86a1 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -61,6 +61,8 @@ jobs: echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\", \"isCommit\": true}" > assets/release.json - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + env: + CONFIG_JSON_SOURCE: BUNDLED - run: pnpm build-storybook - name: Copy playground files run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ce7f56b99..5af8abab1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,6 +30,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod + env: + CONFIG_JSON_SOURCE: BUNDLED - run: pnpm build-storybook - name: Copy playground files run: | @@ -43,24 +45,41 @@ jobs: with: run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod id: deploy + # publish to github + - run: cp vercel.json .vercel/output/static/vercel.json + - uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: .vercel/output/static + force_orphan: true + + - name: Build self-host version + run: pnpm build + - name: Bundle server.js + run: | + pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'" + + - name: Create zip package + run: | + mkdir -p package + cp -r dist package/ + cp bundled-server.js package/server.js + cd package + zip -r ../self-host.zip . + - run: | pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # has possible output: tag id: release + # has output - - run: cp vercel.json .vercel/output/static/vercel.json - - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: .vercel/output/static - force_orphan: true - name: Set publishing config run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - run: pnpm tsx scripts/buildNpmReact.ts ${{ steps.release.outputs.tag }} - if: steps.release.outputs.tag - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + # - run: pnpm tsx scripts/buildNpmReact.ts ${{ steps.release.outputs.tag }} + # if: steps.release.outputs.tag + # env: + # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 4769141f7..346413536 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ RUN npm i -g pnpm@9.0.4 # Build arguments ARG DOWNLOAD_SOUNDS=false ARG DISABLE_SERVICE_WORKER=false +ARG CONFIG_JSON_SOURCE=REMOTE # TODO need flat --no-root-optional RUN node ./scripts/dockerPrepare.mjs RUN pnpm i @@ -22,8 +23,8 @@ RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs # ENTRYPOINT ["pnpm", "run", "run-all"] # only for prod -RUN GITHUB_REPOSITORY=zardoy/minecraft-web-client \ - DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \ +RUN DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \ + CONFIG_JSON_SOURCE=$CONFIG_JSON_SOURCE \ pnpm run build # ---- Run Stage ---- diff --git a/README.MD b/README.MD index bd884b438..90e8f35fa 100644 --- a/README.MD +++ b/README.MD @@ -144,6 +144,7 @@ General: - **`?setting=:`** - Set and lock the setting on load. You can set multiple settings by separating them with `&` e.g. `?setting=autoParkour:true&setting=renderDistance:4` - `?modal=` - Open specific modal on page load eg `keybindings`. Very useful on UI changes testing during dev. For path use `,` as separator. To get currently opened modal type this in the console: `activeModalStack.at(-1).reactType` +- `?replayFileUrl=` - Load and start a packet replay session from a URL with a integrated server. For debugging / previewing recorded sessions. The file must be CORS enabled. Server specific: diff --git a/config.json b/config.json index d6d68b94e..e48d758b3 100644 --- a/config.json +++ b/config.json @@ -6,6 +6,10 @@ "peerJsServer": "", "peerJsServerFallback": "https://p2p.mcraft.fun", "promoteServers": [ + { + "ip": "ws://mcraft.ryzyn.xyz", + "version": "1.19.4" + }, { "ip": "ws://play.mcraft.fun" }, @@ -17,5 +21,15 @@ "version": "1.20.3", "description": "Very nice a polite server. Must try for everyone!" } + ], + "pauseLinks": [ + [ + { + "type": "github" + }, + { + "type": "discord" + } + ] ] } diff --git a/experiments/state.html b/experiments/state.html new file mode 100644 index 000000000..7a5282b77 --- /dev/null +++ b/experiments/state.html @@ -0,0 +1 @@ + diff --git a/experiments/state.ts b/experiments/state.ts new file mode 100644 index 000000000..b01523fce --- /dev/null +++ b/experiments/state.ts @@ -0,0 +1,37 @@ +import { SmoothSwitcher } from '../renderer/viewer/lib/smoothSwitcher' + +const div = document.createElement('div') +div.style.width = '100px' +div.style.height = '100px' +div.style.backgroundColor = 'red' +document.body.appendChild(div) + +const pos = {x: 0, y: 0} + +const positionSwitcher = new SmoothSwitcher(() => pos, (key, value) => { + pos[key] = value +}) +globalThis.positionSwitcher = positionSwitcher + +document.body.addEventListener('keydown', e => { + if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { + const to = { + x: e.code === 'ArrowLeft' ? -100 : 100 + } + console.log(pos, to) + positionSwitcher.transitionTo(to, e.code === 'ArrowLeft' ? 'Left' : 'Right', () => { + console.log('Switched to ', e.code === 'ArrowLeft' ? 'Left' : 'Right') + }) + } + if (e.code === 'Space') { + pos.x = 200 + } +}) + +const render = () => { + positionSwitcher.update() + div.style.transform = `translate(${pos.x}px, ${pos.y}px)` + requestAnimationFrame(render) +} + +render() diff --git a/index.html b/index.html index f168b6e44..50d96a6ac 100644 --- a/index.html +++ b/index.html @@ -38,8 +38,9 @@ } // load error handling const onError = (errorOrMessage, log = false) => { - const message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage + let message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage if (log) console.log(message) + if (typeof message !== 'string') message = String(message) if (document.querySelector('.initial-loader') && document.querySelector('.initial-loader').querySelector('.title').textContent !== 'Error') { document.querySelector('.initial-loader').querySelector('.title').textContent = 'Error' const [errorMessage, ...errorStack] = message.split('\n') diff --git a/package.json b/package.json index 79e2a9bad..1a1958841 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "web", "client" ], + "release": { + "attachReleaseFiles": "self-host.zip" + }, "publish": { "preset": { "publishOnlyIfChanged": true, @@ -81,7 +84,7 @@ "mojangson": "^2.0.4", "net-browserify": "github:zardoy/prismarinejs-net-browserify", "node-gzip": "^1.1.2", - "mcraft-fun-mineflayer": "0.0.3", + "mcraft-fun-mineflayer": "^0.1.8", "peerjs": "^1.5.0", "pixelarticons": "^1.8.1", "pretty-bytes": "^6.1.1", @@ -145,7 +148,8 @@ "http-browserify": "^1.7.0", "http-server": "^14.1.1", "https-browserify": "^1.0.0", - "mc-assets": "^0.2.34", + "mc-assets": "^0.2.42", + "mineflayer-mouse": "^0.0.5", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer", "mineflayer-pathfinder": "^2.4.4", @@ -188,6 +192,7 @@ "pnpm": { "overrides": { "buffer": "^6.0.3", + "vec3": "0.1.10", "@nxg-org/mineflayer-physics-util": "1.5.8", "three": "0.154.0", "diamond-square": "github:zardoy/diamond-square", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7540c5bb5..d5c2446f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: buffer: ^6.0.3 + vec3: 0.1.10 '@nxg-org/mineflayer-physics-util': 1.5.8 three: 0.154.0 diamond-square: github:zardoy/diamond-square @@ -134,14 +135,14 @@ importers: specifier: ^4.17.21 version: 4.17.21 mcraft-fun-mineflayer: - specifier: 0.0.3 - version: 0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13)) + specifier: ^0.1.8 + version: 0.1.8(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13)) minecraft-data: specifier: 3.83.1 version: 3.83.1 minecraft-protocol: specifier: github:PrismarineJS/node-minecraft-protocol#master - version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) + version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) mineflayer-item-map-downloader: specifier: github:zardoy/mineflayer-item-map-downloader version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=bck55yjvd4wrgz46x7o4vfur5q)(encoding@0.1.13) @@ -233,8 +234,8 @@ importers: specifier: ^1.11.1 version: 1.11.2(@types/react@18.2.20)(react@18.2.0) vec3: - specifier: ^0.1.7 - version: 0.1.8 + specifier: 0.1.10 + version: 0.1.10 wait-on: specifier: ^7.2.0 version: 7.2.0(debug@4.4.0) @@ -281,7 +282,7 @@ importers: version: 7.4.6(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) '@storybook/react-vite': specifier: ^7.4.6 - version: 7.4.6(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rollup@2.79.1)(typescript@5.5.4)(vite@4.5.3(@types/node@22.8.1)(terser@5.31.3)) + version: 7.4.6(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rollup@2.79.1)(typescript@5.5.4)(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1)) '@types/diff-match-patch': specifier: ^1.0.36 version: 1.0.36 @@ -349,14 +350,17 @@ importers: specifier: ^1.0.0 version: 1.0.0 mc-assets: - specifier: ^0.2.34 - version: 0.2.34 + specifier: ^0.2.42 + version: 0.2.42 minecraft-inventory-gui: specifier: github:zardoy/minecraft-inventory-gui#next version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/75e940a4cd50d89e0ba03db3733d5d704917a3c8(@types/react@18.2.20)(react@18.2.0) mineflayer: specifier: github:zardoy/mineflayer - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13) + mineflayer-mouse: + specifier: ^0.0.5 + version: 0.0.5(@types/debug@4.1.12)(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) mineflayer-pathfinder: specifier: ^2.4.4 version: 2.4.4 @@ -460,8 +464,8 @@ importers: specifier: ^4.7.0 version: 4.7.0 vec3: - specifier: ^0.1.7 - version: 0.1.8 + specifier: 0.1.10 + version: 0.1.10 optionalDependencies: canvas: specifier: ^2.11.2 @@ -1264,6 +1268,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.0': + resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -1276,6 +1286,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.0': + resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -1288,6 +1304,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.0': + resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -1300,6 +1322,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.0': + resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -1312,6 +1340,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.0': + resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -1324,6 +1358,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.0': + resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -1336,6 +1376,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.0': + resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -1348,6 +1394,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.0': + resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -1360,6 +1412,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.0': + resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -1372,6 +1430,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.0': + resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -1384,6 +1448,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.0': + resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -1396,6 +1466,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.0': + resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -1408,6 +1484,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.0': + resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -1420,6 +1502,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.0': + resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -1432,6 +1520,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.0': + resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -1444,6 +1538,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.0': + resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -1456,6 +1556,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.0': + resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.0': + resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -1468,6 +1580,18 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.0': + resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.0': + resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -1480,6 +1604,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.0': + resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -1492,6 +1622,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.0': + resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -1504,6 +1640,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.0': + resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -1516,6 +1658,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.0': + resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -1528,6 +1676,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.0': + resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1812,6 +1966,9 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -2243,6 +2400,101 @@ packages: rollup: optional: true + '@rollup/rollup-android-arm-eabi@4.34.8': + resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.34.8': + resolution: {integrity: sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.34.8': + resolution: {integrity: sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.34.8': + resolution: {integrity: sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.34.8': + resolution: {integrity: sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.34.8': + resolution: {integrity: sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.34.8': + resolution: {integrity: sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.34.8': + resolution: {integrity: sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.34.8': + resolution: {integrity: sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.34.8': + resolution: {integrity: sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.34.8': + resolution: {integrity: sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.34.8': + resolution: {integrity: sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.34.8': + resolution: {integrity: sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.34.8': + resolution: {integrity: sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.34.8': + resolution: {integrity: sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.34.8': + resolution: {integrity: sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.34.8': + resolution: {integrity: sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.34.8': + resolution: {integrity: sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.34.8': + resolution: {integrity: sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==} + cpu: [x64] + os: [win32] + '@rsbuild/core@1.0.1-beta.9': resolution: {integrity: sha512-F9npL47TFmNVhPBqoE6jBvKGxXEKNszBA7skhbi3opskmX7Ako9vfXvtgi2W2jQjq837/WUL8gG/ua9zRqKFEQ==} engines: {node: '>=16.7.0'} @@ -2721,6 +2973,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/express-serve-static-core@4.17.37': resolution: {integrity: sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==} @@ -3022,18 +3277,47 @@ packages: '@vitest/expect@0.34.6': resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} + '@vitest/expect@3.0.7': + resolution: {integrity: sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==} + + '@vitest/mocker@3.0.7': + resolution: {integrity: sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.0.7': + resolution: {integrity: sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==} + '@vitest/runner@0.34.6': resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} + '@vitest/runner@3.0.7': + resolution: {integrity: sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==} + '@vitest/snapshot@0.34.6': resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} + '@vitest/snapshot@3.0.7': + resolution: {integrity: sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==} + '@vitest/spy@0.34.6': resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} + '@vitest/spy@3.0.7': + resolution: {integrity: sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==} + '@vitest/utils@0.34.6': resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} + '@vitest/utils@3.0.7': + resolution: {integrity: sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==} + '@webassemblyjs/ast@1.12.1': resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -3414,6 +3698,10 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + assign-symbols@1.0.0: resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} engines: {node: '>=0.10.0'} @@ -3718,8 +4006,8 @@ packages: resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} engines: {node: '>=6'} - call-bind-apply-helpers@1.0.1: - resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} call-bind@1.0.8: @@ -3773,6 +4061,10 @@ packages: resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3791,6 +4083,9 @@ packages: change-case@5.1.2: resolution: {integrity: sha512-CAtbGEDulyjzs05RXy3uKcwqeztz/dMEuAc1Xu9NQBsbrhuGMneL0u9Dj5SoutLKBFYun8txxYIwhjtLNfUmCA==} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} @@ -3800,6 +4095,10 @@ packages: check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -4216,6 +4515,10 @@ packages: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4537,6 +4840,9 @@ packages: es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-module-lexer@1.6.0: + resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -4545,8 +4851,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-shim-unscopables@1.0.2: - resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} es-to-primitive@1.3.0: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} @@ -4581,6 +4888,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.25.0: + resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -4789,6 +5101,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -4844,6 +5159,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} @@ -5133,8 +5452,8 @@ packages: get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - get-intrinsic@1.2.7: - resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} get-nonce@1.0.1: @@ -6156,6 +6475,9 @@ packages: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} deprecated: Please upgrade to 2.3.7 which fixes GHSA-4q6p-r6v2-jvc5 + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -6184,6 +6506,9 @@ packages: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.4: resolution: {integrity: sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==} engines: {node: '>=12'} @@ -6243,13 +6568,16 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mc-assets@0.2.34: - resolution: {integrity: sha512-BvE2mVs9XETLFb+FN1Zbc4mJ+CvZqgxVd3kxhERp1QljudDmMsWsMcK2EUTrevuE6a7L3F2kx8XC1vVA79i/ow==} + maxrects-packer@2.7.3: + resolution: {integrity: sha512-bG6qXujJ1QgttZVIH4WDanhoJtvbud/xP/XPyf6A69C9RdA61BM4TomFALCq2nrTa+tARRIBB4LuIFsnUQU2wA==} + + mc-assets@0.2.42: + resolution: {integrity: sha512-j2D1RNYtB5Z9gFu9MVjyDBbiALI0mWZ3xW/A3PPefVAHm3HJ2T1vH+1XBov1spBGPl7u+Zo7mRXza3X0egbeOg==} engines: {node: '>=18.0.0'} - mcraft-fun-mineflayer@0.0.3: - resolution: {integrity: sha512-IqYXHk5ihQOF9FzEpMWsFwgilTySklYVj3AODK7sdgaSe+pU9wZllJjVvsYdc/F3uLMgogkyTLuVoEVMD+UiSA==} - version: 0.0.3 + mcraft-fun-mineflayer@0.1.8: + resolution: {integrity: sha512-jyJTihNHfeToBPwVs3QMKBlVcaCABJ25YN2eoIBQEVTRVFzaXh13XRpElphLzTMj1Q5XFYqufHtMoR4tsb08qQ==} + version: 0.1.8 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} peerDependencies: '@roamhq/wrtc': '*' @@ -6457,8 +6785,8 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/75e940a4cd50d89e0ba03db3733d5d704917a3c8} version: 1.0.1 - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76: - resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76} + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d: + resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d} version: 1.54.0 engines: {node: '>=22'} @@ -6473,6 +6801,10 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824} version: 1.2.0 + mineflayer-mouse@0.0.5: + resolution: {integrity: sha512-0r/AOGTq+wZH9vrBcW93jH2dGRSlwlO6xc1Z67VJUFlZZ8oBefAOpiZq7LIGc7ROVbpcKEKjROdNv/iCFmzXYA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + mineflayer-pathfinder@2.4.4: resolution: {integrity: sha512-HAXakZrJRb1UC+5dv8EaDrqjW3ZnBnBk3nkb6x/YWyhHCUKn/E7VU0FO+UN9whuqPlkSaVumEdXJdydE6lSYxQ==} @@ -6480,8 +6812,8 @@ packages: resolution: {integrity: sha512-q7cmpZFaSI6sodcMJxc2GkV8IO84HbsUP+xNipGKfGg+FMISKabzdJ838Axb60qRtZrp6ny7LluQE7lesHvvxQ==} engines: {node: '>=18'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49} version: 4.25.0 engines: {node: '>=18'} @@ -6610,6 +6942,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanomatch@1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} engines: {node: '>=0.10.0'} @@ -7001,9 +7338,16 @@ packages: pathe@1.1.1: resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + pause-stream@0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} @@ -7117,6 +7461,10 @@ packages: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + potpack@1.0.2: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} @@ -7805,6 +8153,11 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true + rollup@4.34.8: + resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} @@ -8194,6 +8547,9 @@ packages: std-env@3.4.3: resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + store2@2.14.2: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} @@ -8436,17 +8792,35 @@ packages: tinybench@2.5.1: resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinypool@0.7.0: resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} engines: {node: '>=14.0.0'} + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyspy@2.2.0: resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} @@ -8883,8 +9257,8 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vec3@0.1.8: - resolution: {integrity: sha512-LfKrP625Bsg/Tj52YdYPsHmpsJuo+tc6fLxZxXjEo9k2xSspKlPvoYTHehykKhp1FvV9nm+XU3Ehej5/9tpDCg==} + vec3@0.1.10: + resolution: {integrity: sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==} verror@1.10.0: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} @@ -8901,6 +9275,11 @@ packages: engines: {node: '>=v14.18.0'} hasBin: true + vite-node@3.0.7: + resolution: {integrity: sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite@4.5.3: resolution: {integrity: sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -8929,6 +9308,46 @@ packages: terser: optional: true + vite@6.2.0: + resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitest@0.34.6: resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} engines: {node: '>=v14.18.0'} @@ -8960,6 +9379,34 @@ packages: webdriverio: optional: true + vitest@3.0.7: + resolution: {integrity: sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.7 + '@vitest/ui': 3.0.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -9062,6 +9509,11 @@ packages: engines: {node: '>=8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} @@ -10250,138 +10702,213 @@ snapshots: '@esbuild/aix-ppc64@0.19.11': optional: true + '@esbuild/aix-ppc64@0.25.0': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true '@esbuild/android-arm64@0.19.11': optional: true + '@esbuild/android-arm64@0.25.0': + optional: true + '@esbuild/android-arm@0.18.20': optional: true '@esbuild/android-arm@0.19.11': optional: true + '@esbuild/android-arm@0.25.0': + optional: true + '@esbuild/android-x64@0.18.20': optional: true '@esbuild/android-x64@0.19.11': optional: true + '@esbuild/android-x64@0.25.0': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true '@esbuild/darwin-arm64@0.19.11': optional: true + '@esbuild/darwin-arm64@0.25.0': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true '@esbuild/darwin-x64@0.19.11': optional: true + '@esbuild/darwin-x64@0.25.0': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true '@esbuild/freebsd-arm64@0.19.11': optional: true + '@esbuild/freebsd-arm64@0.25.0': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true '@esbuild/freebsd-x64@0.19.11': optional: true + '@esbuild/freebsd-x64@0.25.0': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true '@esbuild/linux-arm64@0.19.11': optional: true + '@esbuild/linux-arm64@0.25.0': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true '@esbuild/linux-arm@0.19.11': optional: true + '@esbuild/linux-arm@0.25.0': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true '@esbuild/linux-ia32@0.19.11': optional: true + '@esbuild/linux-ia32@0.25.0': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true '@esbuild/linux-loong64@0.19.11': optional: true + '@esbuild/linux-loong64@0.25.0': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true '@esbuild/linux-mips64el@0.19.11': optional: true + '@esbuild/linux-mips64el@0.25.0': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true '@esbuild/linux-ppc64@0.19.11': optional: true + '@esbuild/linux-ppc64@0.25.0': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true '@esbuild/linux-riscv64@0.19.11': optional: true + '@esbuild/linux-riscv64@0.25.0': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true '@esbuild/linux-s390x@0.19.11': optional: true + '@esbuild/linux-s390x@0.25.0': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true '@esbuild/linux-x64@0.19.11': optional: true + '@esbuild/linux-x64@0.25.0': + optional: true + + '@esbuild/netbsd-arm64@0.25.0': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true '@esbuild/netbsd-x64@0.19.11': optional: true + '@esbuild/netbsd-x64@0.25.0': + optional: true + + '@esbuild/openbsd-arm64@0.25.0': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true '@esbuild/openbsd-x64@0.19.11': optional: true + '@esbuild/openbsd-x64@0.25.0': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true '@esbuild/sunos-x64@0.19.11': optional: true + '@esbuild/sunos-x64@0.25.0': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true '@esbuild/win32-arm64@0.19.11': optional: true + '@esbuild/win32-arm64@0.25.0': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true '@esbuild/win32-ia32@0.19.11': optional: true + '@esbuild/win32-ia32@0.25.0': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true '@esbuild/win32-x64@0.19.11': optional: true + '@esbuild/win32-x64@0.25.0': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.50.0)': dependencies: eslint: 8.50.0 @@ -10809,13 +11336,13 @@ snapshots: regenerator-runtime: 0.13.11 optional: true - '@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.5.4)(vite@4.5.3(@types/node@22.8.1)(terser@5.31.3))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.5.4)(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1))': dependencies: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.5.4) - vite: 4.5.3(@types/node@22.8.1)(terser@5.31.3) + vite: 6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) optionalDependencies: typescript: 5.5.4 @@ -10836,6 +11363,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.1 @@ -11294,6 +11823,63 @@ snapshots: optionalDependencies: rollup: 2.79.1 + '@rollup/rollup-android-arm-eabi@4.34.8': + optional: true + + '@rollup/rollup-android-arm64@4.34.8': + optional: true + + '@rollup/rollup-darwin-arm64@4.34.8': + optional: true + + '@rollup/rollup-darwin-x64@4.34.8': + optional: true + + '@rollup/rollup-freebsd-arm64@4.34.8': + optional: true + + '@rollup/rollup-freebsd-x64@4.34.8': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.34.8': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.34.8': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.34.8': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.34.8': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.34.8': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.34.8': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.34.8': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.34.8': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.34.8': + optional: true + + '@rollup/rollup-linux-x64-musl@4.34.8': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.34.8': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.34.8': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.34.8': + optional: true + '@rsbuild/core@1.0.1-beta.9': dependencies: '@rspack/core': 1.0.0-beta.1(@swc/helpers@0.5.11) @@ -11691,7 +12277,7 @@ snapshots: - encoding - supports-color - '@storybook/builder-vite@7.4.6(encoding@0.1.13)(typescript@5.5.4)(vite@4.5.3(@types/node@22.8.1)(terser@5.31.3))': + '@storybook/builder-vite@7.4.6(encoding@0.1.13)(typescript@5.5.4)(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1))': dependencies: '@storybook/channels': 7.4.6 '@storybook/client-logger': 7.4.6 @@ -11712,7 +12298,7 @@ snapshots: remark-external-links: 8.0.0 remark-slug: 6.1.0 rollup: 3.29.4 - vite: 4.5.3(@types/node@22.8.1)(terser@5.31.3) + vite: 6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: @@ -11998,19 +12584,19 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@storybook/react-vite@7.4.6(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rollup@2.79.1)(typescript@5.5.4)(vite@4.5.3(@types/node@22.8.1)(terser@5.31.3))': + '@storybook/react-vite@7.4.6(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rollup@2.79.1)(typescript@5.5.4)(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.5.4)(vite@4.5.3(@types/node@22.8.1)(terser@5.31.3)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.5.4)(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1)) '@rollup/pluginutils': 5.0.5(rollup@2.79.1) - '@storybook/builder-vite': 7.4.6(encoding@0.1.13)(typescript@5.5.4)(vite@4.5.3(@types/node@22.8.1)(terser@5.31.3)) + '@storybook/builder-vite': 7.4.6(encoding@0.1.13)(typescript@5.5.4)(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1)) '@storybook/react': 7.4.6(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) - '@vitejs/plugin-react': 3.1.0(vite@4.5.3(@types/node@22.8.1)(terser@5.31.3)) + '@vitejs/plugin-react': 3.1.0(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1)) ast-types: 0.14.2 magic-string: 0.30.4 react: 18.2.0 react-docgen: 6.0.0-alpha.3 react-dom: 18.2.0(react@18.2.0) - vite: 4.5.3(@types/node@22.8.1)(terser@5.31.3) + vite: 6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -12232,6 +12818,8 @@ snapshots: '@types/estree@1.0.5': {} + '@types/estree@1.0.6': {} + '@types/express-serve-static-core@4.17.37': dependencies: '@types/node': 22.8.1 @@ -12577,14 +13165,14 @@ snapshots: '@typescript-eslint/types': 8.0.0 eslint-visitor-keys: 3.4.3 - '@vitejs/plugin-react@3.1.0(vite@4.5.3(@types/node@22.8.1)(terser@5.31.3))': + '@vitejs/plugin-react@3.1.0(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1))': dependencies: '@babel/core': 7.22.11 '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.11) '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.11) magic-string: 0.27.0 react-refresh: 0.14.2 - vite: 4.5.3(@types/node@22.8.1)(terser@5.31.3) + vite: 6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) transitivePeerDependencies: - supports-color @@ -12594,28 +13182,68 @@ snapshots: '@vitest/utils': 0.34.6 chai: 4.3.10 + '@vitest/expect@3.0.7': + dependencies: + '@vitest/spy': 3.0.7 + '@vitest/utils': 3.0.7 + chai: 5.2.0 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.0.7(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1))': + dependencies: + '@vitest/spy': 3.0.7 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) + + '@vitest/pretty-format@3.0.7': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@0.34.6': dependencies: '@vitest/utils': 0.34.6 p-limit: 4.0.0 pathe: 1.1.1 + '@vitest/runner@3.0.7': + dependencies: + '@vitest/utils': 3.0.7 + pathe: 2.0.3 + '@vitest/snapshot@0.34.6': dependencies: magic-string: 0.30.4 pathe: 1.1.1 pretty-format: 29.7.0 + '@vitest/snapshot@3.0.7': + dependencies: + '@vitest/pretty-format': 3.0.7 + magic-string: 0.30.17 + pathe: 2.0.3 + '@vitest/spy@0.34.6': dependencies: tinyspy: 2.2.0 + '@vitest/spy@3.0.7': + dependencies: + tinyspy: 3.0.2 + '@vitest/utils@0.34.6': dependencies: diff-sequences: 29.6.3 loupe: 2.3.6 pretty-format: 29.7.0 + '@vitest/utils@3.0.7': + dependencies: + '@vitest/pretty-format': 3.0.7 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + '@webassemblyjs/ast@1.12.1': dependencies: '@webassemblyjs/helper-numbers': 1.11.6 @@ -12771,7 +13399,7 @@ snapshots: flatmap: 0.0.3 long: 5.2.3 minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -12789,7 +13417,7 @@ snapshots: sanitize-filename: 1.6.3 typed-emitter: 1.4.0 uuid-1345: 1.0.2 - vec3: 0.1.8 + vec3: 0.1.10 yaml: 2.4.1 yargs: 17.7.2 transitivePeerDependencies: @@ -12807,7 +13435,7 @@ snapshots: flatmap: 0.0.3 long: 5.2.3 minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -12825,7 +13453,7 @@ snapshots: sanitize-filename: 1.6.3 typed-emitter: 1.4.0 uuid-1345: 1.0.2 - vec3: 0.1.8 + vec3: 0.1.10 yaml: 2.4.1 yargs: 17.7.2 transitivePeerDependencies: @@ -13030,7 +13658,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.9 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-string: 1.1.1 array-union@2.1.0: {} @@ -13044,28 +13672,28 @@ snapshots: es-abstract: 1.23.9 es-errors: 1.3.0 es-object-atoms: 1.1.1 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.flat@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 es-abstract: 1.23.9 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 es-abstract: 1.23.9 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.toreversed@1.1.2: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 es-abstract: 1.23.9 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.tosorted@1.1.4: dependencies: @@ -13073,7 +13701,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.4: dependencies: @@ -13082,7 +13710,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 arraybuffer.slice@0.0.7: @@ -13116,6 +13744,8 @@ snapshots: assertion-error@1.1.0: {} + assertion-error@2.0.1: {} + assign-symbols@1.0.0: {} ast-types@0.14.2: @@ -13514,22 +14144,22 @@ snapshots: cachedir@2.4.0: optional: true - call-bind-apply-helpers@1.0.1: + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 call-bind@1.0.8: dependencies: - call-bind-apply-helpers: 1.0.1 + call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 set-function-length: 1.2.2 call-bound@1.0.3: dependencies: - call-bind-apply-helpers: 1.0.1 - get-intrinsic: 1.2.7 + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 callsites@3.1.0: {} @@ -13596,6 +14226,14 @@ snapshots: pathval: 1.1.1 type-detect: 4.0.8 + chai@5.2.0: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -13626,6 +14264,8 @@ snapshots: change-case@5.1.2: {} + change-case@5.4.4: {} + character-entities@2.0.2: {} charenc@0.0.2: {} @@ -13634,6 +14274,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + check-error@2.1.1: {} + check-more-types@2.24.0: optional: true @@ -14147,6 +14789,8 @@ snapshots: dependencies: type-detect: 4.0.8 + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -14263,7 +14907,7 @@ snapshots: prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1) prismarine-registry: 1.11.0 random-seed: 0.3.0 - vec3: 0.1.8 + vec3: 0.1.10 diff-match-patch@1.0.5: {} @@ -14344,7 +14988,7 @@ snapshots: dunder-proto@1.0.1: dependencies: - call-bind-apply-helpers: 1.0.1 + call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 @@ -14528,7 +15172,7 @@ snapshots: es-set-tostringtag: 2.1.0 es-to-primitive: 1.3.0 function.prototype.name: 1.1.8 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-proto: 1.0.1 get-symbol-description: 1.1.0 globalthis: 1.0.4 @@ -14579,7 +15223,7 @@ snapshots: es-errors: 1.3.0 es-set-tostringtag: 2.1.0 function-bind: 1.1.2 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 globalthis: 1.0.4 gopd: 1.2.0 has-property-descriptors: 1.0.2 @@ -14593,6 +15237,8 @@ snapshots: es-module-lexer@1.5.4: {} + es-module-lexer@1.6.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -14600,11 +15246,11 @@ snapshots: es-set-tostringtag@2.1.0: dependencies: es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 hasown: 2.0.2 - es-shim-unscopables@1.0.2: + es-shim-unscopables@1.1.0: dependencies: hasown: 2.0.2 @@ -14686,6 +15332,34 @@ snapshots: '@esbuild/win32-ia32': 0.19.11 '@esbuild/win32-x64': 0.19.11 + esbuild@0.25.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.0 + '@esbuild/android-arm': 0.25.0 + '@esbuild/android-arm64': 0.25.0 + '@esbuild/android-x64': 0.25.0 + '@esbuild/darwin-arm64': 0.25.0 + '@esbuild/darwin-x64': 0.25.0 + '@esbuild/freebsd-arm64': 0.25.0 + '@esbuild/freebsd-x64': 0.25.0 + '@esbuild/linux-arm': 0.25.0 + '@esbuild/linux-arm64': 0.25.0 + '@esbuild/linux-ia32': 0.25.0 + '@esbuild/linux-loong64': 0.25.0 + '@esbuild/linux-mips64el': 0.25.0 + '@esbuild/linux-ppc64': 0.25.0 + '@esbuild/linux-riscv64': 0.25.0 + '@esbuild/linux-s390x': 0.25.0 + '@esbuild/linux-x64': 0.25.0 + '@esbuild/netbsd-arm64': 0.25.0 + '@esbuild/netbsd-x64': 0.25.0 + '@esbuild/openbsd-arm64': 0.25.0 + '@esbuild/openbsd-x64': 0.25.0 + '@esbuild/sunos-x64': 0.25.0 + '@esbuild/win32-arm64': 0.25.0 + '@esbuild/win32-ia32': 0.25.0 + '@esbuild/win32-x64': 0.25.0 + escalade@3.1.2: {} escape-html@1.0.3: {} @@ -14961,6 +15635,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + esutils@2.0.3: {} etag@1.8.1: {} @@ -15038,6 +15716,8 @@ snapshots: expand-template@2.0.3: {} + expect-type@1.1.0: {} + exponential-backoff@3.1.1: optional: true @@ -15424,9 +16104,9 @@ snapshots: get-func-name@2.0.2: {} - get-intrinsic@1.2.7: + get-intrinsic@1.3.0: dependencies: - call-bind-apply-helpers: 1.0.1 + call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 @@ -15462,7 +16142,7 @@ snapshots: dependencies: call-bound: 1.0.3 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-tsconfig@4.7.2: dependencies: @@ -15928,7 +16608,7 @@ snapshots: dependencies: call-bind: 1.0.8 call-bound: 1.0.3 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-arrayish@0.2.1: {} @@ -15983,7 +16663,7 @@ snapshots: is-data-view@1.0.2: dependencies: call-bound: 1.0.3 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-typed-array: 1.1.15 is-date-object@1.1.0: @@ -16132,7 +16812,7 @@ snapshots: is-weakset@2.0.4: dependencies: call-bound: 1.0.3 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-windows@1.0.2: {} @@ -16190,7 +16870,7 @@ snapshots: dependencies: define-data-property: 1.1.4 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-proto: 1.0.1 has-symbols: 1.1.0 set-function-name: 2.0.2 @@ -16579,6 +17259,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + loupe@3.1.3: {} + lower-case@2.0.2: dependencies: tslib: 2.6.2 @@ -16606,6 +17288,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.4: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -16679,16 +17365,19 @@ snapshots: math-intrinsics@1.1.0: {} - mc-assets@0.2.34: + maxrects-packer@2.7.3: {} + + mc-assets@0.2.42: dependencies: + maxrects-packer: 2.7.3 zod: 3.24.1 - mcraft-fun-mineflayer@0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.8(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.0 transitivePeerDependencies: @@ -16996,7 +17685,7 @@ snapshots: - '@types/react' - react - minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13): + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 '@types/readable-stream': 4.0.12 @@ -17043,7 +17732,7 @@ snapshots: minecrafthawkeye@1.3.6: dependencies: detect-collisions: 7.0.5 - vec3: 0.1.8 + vec3: 0.1.10 mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=bck55yjvd4wrgz46x7o4vfur5q)(encoding@0.1.13): dependencies: @@ -17053,6 +17742,33 @@ snapshots: - encoding - supports-color + mineflayer-mouse@0.0.5(@types/debug@4.1.12)(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1): + dependencies: + change-case: 5.4.4 + debug: 4.4.0(supports-color@8.1.1) + prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c + vitest: 3.0.7(@types/debug@4.1.12)(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) + transitivePeerDependencies: + - '@edge-runtime/vm' + - '@types/debug' + - '@types/node' + - '@vitest/browser' + - '@vitest/ui' + - happy-dom + - jiti + - jsdom + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + mineflayer-pathfinder@2.4.4: dependencies: minecraft-data: 3.83.1 @@ -17061,12 +17777,12 @@ snapshots: prismarine-item: 1.16.0 prismarine-nbt: 2.5.0 prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b - vec3: 0.1.8 + vec3: 0.1.10 mineflayer@4.25.0(encoding@0.1.13): dependencies: minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-chat: 1.10.1 @@ -17081,15 +17797,15 @@ snapshots: prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c protodef: 1.18.0 typed-emitter: 1.4.0 - vec3: 0.1.8 + vec3: 0.1.10 transitivePeerDependencies: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13): dependencies: minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-chat: 1.10.1 @@ -17104,7 +17820,7 @@ snapshots: prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c protodef: 1.18.0 typed-emitter: 1.4.0 - vec3: 0.1.8 + vec3: 0.1.10 transitivePeerDependencies: - encoding - supports-color @@ -17246,6 +17962,8 @@ snapshots: nanoid@3.3.7: {} + nanoid@3.3.8: {} + nanomatch@1.2.13: dependencies: arr-diff: 4.0.0 @@ -17562,7 +18280,7 @@ snapshots: own-keys@1.0.1: dependencies: - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 @@ -17704,8 +18422,12 @@ snapshots: pathe@1.1.1: {} + pathe@2.0.3: {} + pathval@1.1.1: {} + pathval@2.0.0: {} + pause-stream@0.0.11: dependencies: through: 2.3.8 @@ -17813,6 +18535,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.3: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + potpack@1.0.2: {} prebuild-install@7.1.1: @@ -17892,7 +18620,7 @@ snapshots: prismarine-registry: 1.11.0 smart-buffer: 4.2.0 uint4: 0.1.2 - vec3: 0.1.8 + vec3: 0.1.10 xxhash-wasm: 0.4.2 transitivePeerDependencies: - minecraft-data @@ -17902,7 +18630,7 @@ snapshots: prismarine-chat: 1.10.1 prismarine-item: 1.16.0 prismarine-registry: 1.11.0 - vec3: 0.1.8 + vec3: 0.1.10 prismarine-item@1.16.0: dependencies: @@ -17917,7 +18645,7 @@ snapshots: dependencies: minecraft-data: 3.83.1 prismarine-nbt: 2.5.0 - vec3: 0.1.8 + vec3: 0.1.10 prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1): dependencies: @@ -17926,7 +18654,7 @@ snapshots: prismarine-nbt: 2.5.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c uint4: 0.1.2 - vec3: 0.1.8 + vec3: 0.1.10 transitivePeerDependencies: - minecraft-data @@ -17954,7 +18682,7 @@ snapshots: prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0) prismarine-nbt: 2.5.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c - vec3: 0.1.8 + vec3: 0.1.10 prismarine-windows@2.9.0: dependencies: @@ -17964,7 +18692,7 @@ snapshots: prismarine-world@https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c: dependencies: - vec3: 0.1.8 + vec3: 0.1.10 process-nextick-args@2.0.1: {} @@ -18512,7 +19240,7 @@ snapshots: es-abstract: 1.23.9 es-errors: 1.3.0 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 @@ -18695,6 +19423,31 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + rollup@4.34.8: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.34.8 + '@rollup/rollup-android-arm64': 4.34.8 + '@rollup/rollup-darwin-arm64': 4.34.8 + '@rollup/rollup-darwin-x64': 4.34.8 + '@rollup/rollup-freebsd-arm64': 4.34.8 + '@rollup/rollup-freebsd-x64': 4.34.8 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.8 + '@rollup/rollup-linux-arm-musleabihf': 4.34.8 + '@rollup/rollup-linux-arm64-gnu': 4.34.8 + '@rollup/rollup-linux-arm64-musl': 4.34.8 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.8 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.8 + '@rollup/rollup-linux-riscv64-gnu': 4.34.8 + '@rollup/rollup-linux-s390x-gnu': 4.34.8 + '@rollup/rollup-linux-x64-gnu': 4.34.8 + '@rollup/rollup-linux-x64-musl': 4.34.8 + '@rollup/rollup-win32-arm64-msvc': 4.34.8 + '@rollup/rollup-win32-ia32-msvc': 4.34.8 + '@rollup/rollup-win32-x64-msvc': 4.34.8 + fsevents: 2.3.3 + rope-sequence@1.3.4: {} rtl-css-js@1.16.1: @@ -18713,7 +19466,7 @@ snapshots: dependencies: call-bind: 1.0.8 call-bound: 1.0.3 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 has-symbols: 1.1.0 isarray: 2.0.5 @@ -18849,7 +19602,7 @@ snapshots: define-data-property: 1.1.4 es-errors: 1.3.0 function-bind: 1.1.2 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 gopd: 1.2.0 has-property-descriptors: 1.0.2 @@ -18924,14 +19677,14 @@ snapshots: dependencies: call-bound: 1.0.3 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-inspect: 1.13.3 side-channel-weakmap@1.0.2: dependencies: call-bound: 1.0.3 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-inspect: 1.13.3 side-channel-map: 1.0.1 @@ -19243,6 +19996,8 @@ snapshots: std-env@3.4.3: {} + std-env@3.8.0: {} + store2@2.14.2: {} storybook@7.4.6(encoding@0.1.13): @@ -19294,7 +20049,7 @@ snapshots: es-abstract: 1.23.9 es-errors: 1.3.0 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 gopd: 1.2.0 has-symbols: 1.1.0 internal-slot: 1.1.0 @@ -19529,13 +20284,23 @@ snapshots: tinybench@2.5.1: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: optional: true + tinyexec@0.3.2: {} + tinypool@0.7.0: {} + tinypool@1.0.2: {} + + tinyrainbow@2.0.0: {} + tinyspy@2.2.0: {} + tinyspy@3.0.2: {} + title-case@3.0.3: dependencies: tslib: 2.6.2 @@ -19968,7 +20733,7 @@ snapshots: vary@1.1.2: {} - vec3@0.1.8: {} + vec3@0.1.10: {} verror@1.10.0: dependencies: @@ -20006,6 +20771,27 @@ snapshots: - supports-color - terser + vite-node@3.0.7(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1): + dependencies: + cac: 6.7.14 + debug: 4.4.0(supports-color@8.1.1) + es-module-lexer: 1.6.0 + pathe: 2.0.3 + vite: 6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite@4.5.3(@types/node@22.8.1)(terser@5.31.3): dependencies: esbuild: 0.18.20 @@ -20016,6 +20802,18 @@ snapshots: fsevents: 2.3.3 terser: 5.31.3 + vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1): + dependencies: + esbuild: 0.25.0 + postcss: 8.5.3 + rollup: 4.34.8 + optionalDependencies: + '@types/node': 22.8.1 + fsevents: 2.3.3 + terser: 5.31.3 + tsx: 4.7.0 + yaml: 2.4.1 + vitest@0.34.6(terser@5.31.3): dependencies: '@types/chai': 4.3.6 @@ -20051,6 +20849,45 @@ snapshots: - supports-color - terser + vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1): + dependencies: + '@vitest/expect': 3.0.7 + '@vitest/mocker': 3.0.7(vite@6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1)) + '@vitest/pretty-format': 3.0.7 + '@vitest/runner': 3.0.7 + '@vitest/snapshot': 3.0.7 + '@vitest/spy': 3.0.7 + '@vitest/utils': 3.0.7 + chai: 5.2.0 + debug: 4.4.0(supports-color@8.1.1) + expect-type: 1.1.0 + magic-string: 0.30.17 + pathe: 2.0.3 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 6.2.0(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) + vite-node: 3.0.7(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 22.8.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vm-browserify@1.1.2: {} w3c-keyname@2.2.8: {} @@ -20204,6 +21041,11 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wide-align@1.1.5: dependencies: string-width: 4.2.3 diff --git a/renderer/viewer/lib/animationController.ts b/renderer/viewer/lib/animationController.ts index bfccc72f0..329d0e248 100644 --- a/renderer/viewer/lib/animationController.ts +++ b/renderer/viewer/lib/animationController.ts @@ -5,9 +5,10 @@ export class AnimationController { private isAnimating = false private cancelRequested = false private completionCallbacks: Array<() => void> = [] + private currentCancelCallback: (() => void) | null = null /** Main method */ - async startAnimation (createAnimation: () => tweenJs.Group): Promise { + async startAnimation (createAnimation: () => tweenJs.Group, onCancelled?: () => void): Promise { if (this.isAnimating) { await this.cancelCurrentAnimation() } @@ -15,6 +16,7 @@ export class AnimationController { return new Promise((resolve) => { this.isAnimating = true this.cancelRequested = false + this.currentCancelCallback = onCancelled ?? null this.currentAnimation = createAnimation() this.completionCallbacks.push(() => { @@ -29,6 +31,12 @@ export class AnimationController { async cancelCurrentAnimation (): Promise { if (!this.isAnimating) return + if (this.currentCancelCallback) { + const callback = this.currentCancelCallback + this.currentCancelCallback = null + callback() + } + return new Promise((resolve) => { this.cancelRequested = true this.completionCallbacks.push(() => { @@ -41,7 +49,7 @@ export class AnimationController { if (this.cancelRequested) this.forceFinish() } - forceFinish () { + forceFinish (callComplete = true) { if (!this.isAnimating) return if (this.currentAnimation) { @@ -55,7 +63,9 @@ export class AnimationController { const callbacks = [...this.completionCallbacks] this.completionCallbacks = [] - for (const cb of callbacks) cb() + if (callComplete) { + for (const cb of callbacks) cb() + } } /** Required method */ diff --git a/renderer/viewer/lib/entities.ts b/renderer/viewer/lib/entities.ts index c46644899..b60a5fef3 100644 --- a/renderer/viewer/lib/entities.ts +++ b/renderer/viewer/lib/entities.ts @@ -225,10 +225,11 @@ export class Entities extends EventEmitter { su?: number; sv?: number; size?: number; + modelName?: string; } | { resolvedModel: BlockModel modelName: string - }) + } | undefined) get entitiesByName (): Record { const byName: Record = {} @@ -498,8 +499,10 @@ export class Entities extends EventEmitter { return typeof component === 'string' ? component : component.text ?? '' } - getItemMesh (item, specificProps: ItemSpecificContextProperties) { + getItemMesh (item, specificProps: ItemSpecificContextProperties, previousModel?: string) { const textureUv = this.getItemUv?.(item, specificProps) + if (previousModel && previousModel === textureUv?.modelName) return undefined + if (textureUv && 'resolvedModel' in textureUv) { const mesh = getBlockMeshFromModel(this.viewer.world.material, textureUv.resolvedModel, textureUv.modelName) if (specificProps['minecraft:display_context'] === 'ground') { @@ -514,6 +517,7 @@ export class Entities extends EventEmitter { isBlock: true, itemsTexture: null, itemsTextureFlipped: null, + modelName: textureUv.modelName, } } @@ -555,6 +559,7 @@ export class Entities extends EventEmitter { isBlock: false, itemsTexture, itemsTextureFlipped, + modelName: textureUv.modelName, } } } diff --git a/renderer/viewer/lib/entity/armorModels.json b/renderer/viewer/lib/entity/armorModels.json index a97377250..3e33939e5 100644 --- a/renderer/viewer/lib/entity/armorModels.json +++ b/renderer/viewer/lib/entity/armorModels.json @@ -92,7 +92,7 @@ "origin": [4, 12, -2], "size": [4, 12, 4], "uv": [40, 16], - "inflate": 0.75 + "inflate": 0.85 } ] }, @@ -105,7 +105,7 @@ "origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 16], - "inflate": 0.75 + "inflate": 0.85 } ], "mirror": true @@ -128,7 +128,7 @@ "origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [16, 16], - "inflate": 0.75 + "inflate": 0.5 } ] }, @@ -151,7 +151,7 @@ "pivot": [-1.9, 1, 0], "cubes": [ { - "origin": [-3.9, 0, -2], + "origin": [-3.9, 0, -2.01], "size": [4, 12, 4], "uv": [0, 16], "inflate": 0.5 @@ -177,7 +177,7 @@ "origin": [-0.1, 0, -2], "size": [4, 12, 4], "uv": [0, 16], - "inflate": 0.75 + "inflate": 0.8 } ] }, @@ -187,10 +187,10 @@ "pivot": [-1.9, 1, 0], "cubes": [ { - "origin": [-3.9, 0, -2], + "origin": [-3.9, 0.01, -2.01], "size": [4, 12, 4], "uv": [0, 16], - "inflate": 0.75 + "inflate": 0.8 } ], "mirror": true diff --git a/renderer/viewer/lib/guiRenderer.ts b/renderer/viewer/lib/guiRenderer.ts new file mode 100644 index 000000000..b197040e7 --- /dev/null +++ b/renderer/viewer/lib/guiRenderer.ts @@ -0,0 +1,275 @@ +// Import placeholders - replace with actual imports for your environment +import { ItemRenderer, Identifier, ItemStack, NbtString, Structure, StructureRenderer, ItemRendererResources, BlockDefinition, BlockModel, TextureAtlas, Resources, ItemModel } from 'deepslate' +import { mat4, vec3 } from 'gl-matrix' +import { AssetsParser } from 'mc-assets/dist/assetsParser' +import { getLoadedImage } from 'mc-assets/dist/utils' +import { BlockModel as BlockModelMcAssets, AtlasParser } from 'mc-assets' +import { getLoadedBlockstatesStore, getLoadedModelsStore } from 'mc-assets/dist/stores' +import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator' +import { proxy, ref } from 'valtio' +import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' + +export const activeGuiAtlas = proxy({ + atlas: null as null | { json, image }, +}) + +export const getNonFullBlocksModels = () => { + const version = viewer.world.texturesVersion ?? 'latest' + const itemsDefinitions = viewer.world.itemsDefinitionsStore.data.latest + const blockModelsResolved = {} as Record + const itemsModelsResolved = {} as Record + const fullBlocksWithNonStandardDisplay = [] as string[] + const handledItemsWithDefinitions = new Set() + const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(viewer.world.blockstatesModels), getLoadedModelsStore(viewer.world.blockstatesModels)) + + const standardGuiDisplay = { + 'rotation': [ + 30, + 225, + 0 + ], + 'translation': [ + 0, + 0, + 0 + ], + 'scale': [ + 0.625, + 0.625, + 0.625 + ] + } + + const arrEqual = (a: number[], b: number[]) => a.length === b.length && a.every((x, i) => x === b[i]) + const addModelIfNotFullblock = (name: string, model: BlockModelMcAssets) => { + if (blockModelsResolved[name]) return + if (!model?.elements?.length) return + const isFullBlock = model.elements.length === 1 && arrEqual(model.elements[0].from, [0, 0, 0]) && arrEqual(model.elements[0].to, [16, 16, 16]) + if (isFullBlock) return + model['display'] ??= {} + model['display']['gui'] ??= standardGuiDisplay + blockModelsResolved[name] = model + } + + for (const [name, definition] of Object.entries(itemsDefinitions)) { + const item = getItemDefinition(viewer.world.itemsDefinitionsStore, { + version, + name, + properties: { + 'minecraft:display_context': 'gui', + }, + }) + if (item) { + const { resolvedModel } = assetsParser.getResolvedModelsByModel((item.special ? name : item.model).replace('minecraft:', '')) ?? {} + if (resolvedModel) { + handledItemsWithDefinitions.add(name) + } + if (resolvedModel?.elements) { + + let hasStandardDisplay = true + if (resolvedModel['display']?.gui) { + hasStandardDisplay = + arrEqual(resolvedModel['display'].gui.rotation, standardGuiDisplay.rotation) + && arrEqual(resolvedModel['display'].gui.translation, standardGuiDisplay.translation) + && arrEqual(resolvedModel['display'].gui.scale, standardGuiDisplay.scale) + } + + addModelIfNotFullblock(name, resolvedModel) + + if (!blockModelsResolved[name] && !hasStandardDisplay) { + fullBlocksWithNonStandardDisplay.push(name) + } + const notSideLight = resolvedModel['gui_light'] && resolvedModel['gui_light'] !== 'side' + if (!hasStandardDisplay || notSideLight) { + blockModelsResolved[name] = resolvedModel + } + } + if (!blockModelsResolved[name] && item.tints && resolvedModel) { + resolvedModel['tints'] = item.tints + if (resolvedModel.elements) { + blockModelsResolved[name] = resolvedModel + } else { + itemsModelsResolved[name] = resolvedModel + } + } + } + } + + for (const [name, blockstate] of Object.entries(viewer.world.blockstatesModels.blockstates.latest)) { + if (handledItemsWithDefinitions.has(name)) { + continue + } + const resolvedModel = assetsParser.getResolvedModelFirst({ name: name.replace('minecraft:', ''), properties: {} }, true) + if (resolvedModel) { + addModelIfNotFullblock(name, resolvedModel[0]) + } + } + + return { + blockModelsResolved, + itemsModelsResolved + } +} + +// customEvents.on('gameLoaded', () => { +// const res = getNonFullBlocksModels() +// }) + +const RENDER_SIZE = 64 + +const generateItemsGui = async (models: Record, isItems = false) => { + const img = await getLoadedImage(isItems ? viewer.world.itemsAtlasParser!.latestImage : viewer.world.blocksAtlasParser!.latestImage) + const canvasTemp = document.createElement('canvas') + canvasTemp.width = img.width + canvasTemp.height = img.height + canvasTemp.style.imageRendering = 'pixelated' + const ctx = canvasTemp.getContext('2d')! + ctx.imageSmoothingEnabled = false + ctx.drawImage(img, 0, 0) + + const atlasParser = isItems ? viewer.world.itemsAtlasParser! : viewer.world.blocksAtlasParser! + const textureAtlas = new TextureAtlas( + ctx.getImageData(0, 0, img.width, img.height), + Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => { + return [key, [ + value.u, + value.v, + (value.u + (value.su ?? atlasParser.atlas.latest.suSv)), + (value.v + (value.sv ?? atlasParser.atlas.latest.suSv)), + ]] as [string, [number, number, number, number]] + })) + ) + + const PREVIEW_ID = Identifier.parse('preview:preview') + const PREVIEW_DEFINITION = new BlockDefinition({ '': { model: PREVIEW_ID.toString() } }, undefined) + + let modelData: any + let currentModelName: string | undefined + const resources: ItemRendererResources = { + getBlockModel (id) { + if (id.equals(PREVIEW_ID)) { + return BlockModel.fromJson(modelData ?? {}) + } + return null + }, + getTextureUV (texture) { + return textureAtlas.getTextureUV(texture.toString().slice(1).split('/').slice(1).join('/') as any) + }, + getTextureAtlas () { + return textureAtlas.getTextureAtlas() + }, + getItemComponents (id) { + return new Map() + }, + getItemModel (id) { + // const isSpecial = currentModelName === 'shield' || currentModelName === 'conduit' || currentModelName === 'trident' + const isSpecial = false + if (id.equals(PREVIEW_ID)) { + return ItemModel.fromJson({ + type: isSpecial ? 'minecraft:special' : 'minecraft:model', + model: isSpecial ? { + type: currentModelName, + } : PREVIEW_ID.toString(), + base: PREVIEW_ID.toString(), + tints: modelData?.tints, + }) + } + return null + }, + } + + const canvas = document.createElement('canvas') + canvas.width = RENDER_SIZE + canvas.height = RENDER_SIZE + const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true }) + if (!gl) { + throw new Error('Cannot get WebGL2 context') + } + + function resetGLContext (gl) { + gl.clearColor(0, 0, 0, 0) + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) + } + + // const includeOnly = ['powered_repeater', 'wooden_door'] + const includeOnly = [] as string[] + + const images: Record = {} + const item = new ItemStack(PREVIEW_ID, 1, new Map(Object.entries({ + 'minecraft:item_model': new NbtString(PREVIEW_ID.toString()), + }))) + const renderer = new ItemRenderer(gl, item, resources, { display_context: 'gui' }) + const missingTextures = new Set() + for (const [modelName, model] of Object.entries(models)) { + if (includeOnly.length && !includeOnly.includes(modelName)) continue + + const patchMissingTextures = () => { + for (const element of model.elements ?? []) { + for (const [faceName, face] of Object.entries(element.faces)) { + if (face.texture.startsWith('#')) { + missingTextures.add(`${modelName} ${faceName}: ${face.texture}`) + face.texture = 'block/unknown' + } + } + } + } + patchMissingTextures() + // TODO eggs + + modelData = model + currentModelName = modelName + resetGLContext(gl) + if (!modelData) continue + renderer.setItem(item, { display_context: 'gui' }) + renderer.drawItem() + const url = canvas.toDataURL() + // eslint-disable-next-line no-await-in-loop + const img = await getLoadedImage(url) + images[modelName] = img + } + + if (missingTextures.size) { + console.warn(`[guiRenderer] Missing textures in ${[...missingTextures].join(', ')}`) + } + + return images +} + +const generateAtlas = async (images: Record) => { + const atlas = makeTextureAtlas({ + input: Object.keys(images), + tileSize: RENDER_SIZE, + getLoadedImage (name) { + return { + image: images[name], + } + }, + }) + + // const atlasParser = new AtlasParser({ latest: atlas.json }, atlas.canvas.toDataURL()) + // const a = document.createElement('a') + // a.href = await atlasParser.createDebugImage(true) + // a.download = 'blocks_atlas.png' + // a.click() + + activeGuiAtlas.atlas = { + json: atlas.json, + image: ref(await getLoadedImage(atlas.canvas.toDataURL())), + } + + return atlas +} + +export const generateGuiAtlas = async () => { + const { blockModelsResolved, itemsModelsResolved } = getNonFullBlocksModels() + + // Generate blocks atlas + console.time('generate blocks gui atlas') + const blockImages = await generateItemsGui(blockModelsResolved, false) + console.timeEnd('generate blocks gui atlas') + console.time('generate items gui atlas') + const itemImages = await generateItemsGui(itemsModelsResolved, true) + console.timeEnd('generate items gui atlas') + await generateAtlas({ ...blockImages, ...itemImages }) + // await generateAtlas(blockImages) +} diff --git a/renderer/viewer/lib/holdingBlock.ts b/renderer/viewer/lib/holdingBlock.ts index c9fdc5f57..084953fdc 100644 --- a/renderer/viewer/lib/holdingBlock.ts +++ b/renderer/viewer/lib/holdingBlock.ts @@ -235,7 +235,7 @@ export default class HoldingBlock { // new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start() // } - async playBlockSwapAnimation (forceState?: 'appeared' | 'disappeared') { + async playBlockSwapAnimation (forceState: 'appeared' | 'disappeared') { this.blockSwapAnimation ??= { switcher: new SmoothSwitcher( () => ({ @@ -250,23 +250,29 @@ export default class HoldingBlock { ) } - const newState = this.blockSwapAnimation.switcher.currentStateName === 'disappeared' ? 'appeared' : 'disappeared' - if (forceState && newState !== forceState) throw new Error(`forceState does not match current state ${newState} !== ${forceState}`) + const newState = forceState + // if (forceState && newState !== forceState) { + // throw new Error(`forceState does not match current state ${forceState} !== ${newState}`) + // } const targetY = this.objectInnerGroup.position.y + (this.objectInnerGroup.scale.y * 1.5 * (newState === 'appeared' ? 1 : -1)) - if (newState === this.blockSwapAnimation.switcher.transitioningToStateName) { - return false - } + // if (newState === this.blockSwapAnimation.switcher.transitioningToStateName) { + // return false + // } + let cancelled = false return new Promise((resolve) => { this.blockSwapAnimation!.switcher.transitionTo( { y: targetY }, newState, () => { - resolve(true) + if (!cancelled) { + resolve(true) + } }, () => { + cancelled = true resolve(false) } ) @@ -274,7 +280,18 @@ export default class HoldingBlock { } isDifferentItem (block: HandItemBlock | undefined) { - return !this.lastHeldItem || (this.lastHeldItem.name !== block?.name || JSON.stringify(this.lastHeldItem.fullItem) !== JSON.stringify(block?.fullItem ?? '{}')) + if (!this.lastHeldItem) { + return true + } + if (this.lastHeldItem.name !== block?.name) { + return true + } + // eslint-disable-next-line sonarjs/prefer-single-boolean-return + if (JSON.stringify(this.lastHeldItem.fullItem) !== JSON.stringify(block?.fullItem ?? '{}')) { + return true + } + + return false } updateCameraGroup () { @@ -303,31 +320,37 @@ export default class HoldingBlock { this.objectOuterGroup.scale.set(scale, scale, scale) } + lastItemModelName: string | undefined private async createItemModel (handItem: HandItemBlock): Promise<{ model: THREE.Object3D; type: 'hand' | 'block' | 'item' } | undefined> { this.lastUpdate = performance.now() if (!handItem || (handItem.type === 'hand' && !this.playerHand)) return undefined - let blockInner: THREE.Object3D + let blockInner: THREE.Object3D | undefined if (handItem.type === 'item' || handItem.type === 'block') { - const { mesh: itemMesh, isBlock } = viewer.entities.getItemMesh({ + const result = viewer.entities.getItemMesh({ ...handItem.fullItem, itemId: handItem.id, }, { 'minecraft:display_context': 'firstperson', 'minecraft:use_duration': this.playerState.getItemUsageTicks?.(), 'minecraft:using_item': !!this.playerState.getItemUsageTicks?.(), - })! - if (isBlock) { - blockInner = itemMesh - handItem.type = 'block' - } else { - itemMesh.position.set(0.5, 0.5, 0.5) - blockInner = itemMesh - handItem.type = 'item' + }, this.lastItemModelName) + if (result) { + const { mesh: itemMesh, isBlock, modelName } = result + if (isBlock) { + blockInner = itemMesh + handItem.type = 'block' + } else { + itemMesh.position.set(0.5, 0.5, 0.5) + blockInner = itemMesh + handItem.type = 'item' + } + this.lastItemModelName = modelName } } else { blockInner = this.playerHand! } + if (!blockInner) return blockInner.name = 'holdingBlock' const rotationDeg = this.getHandHeld3d().rotation @@ -339,6 +362,9 @@ export default class HoldingBlock { } async replaceItemModel (handItem?: HandItemBlock): Promise { + // if switch animation is in progress, do not replace the item + if (this.blockSwapAnimation?.switcher.isTransitioning) return + if (!handItem) { this.holdingBlock?.removeFromParent() this.holdingBlock = undefined @@ -359,19 +385,31 @@ export default class HoldingBlock { } + testUnknownBlockSwitch () { + void this.setNewItem({ + type: 'item', + name: 'minecraft:some-unknown-block', + id: 0, + fullItem: {} + }) + } + + switchRequest = 0 async setNewItem (handItem?: HandItemBlock) { if (!this.isDifferentItem(handItem)) return + this.lastItemModelName = undefined + const switchRequest = ++this.switchRequest + this.lastHeldItem = handItem let playAppearAnimation = false if (this.holdingBlock) { // play disappear animation playAppearAnimation = true - const result = await this.playBlockSwapAnimation() + const result = await this.playBlockSwapAnimation('disappeared') if (!result) return this.holdingBlock?.removeFromParent() this.holdingBlock = undefined } - this.lastHeldItem = handItem if (!handItem) { this.swingAnimator?.stopSwing() this.swingAnimator = undefined @@ -380,8 +418,9 @@ export default class HoldingBlock { return } + if (switchRequest !== this.switchRequest) return const result = await this.createItemModel(handItem) - if (!result) return + if (!result || switchRequest !== this.switchRequest) return const blockOuterGroup = new THREE.Group() this.holdingBlockInnerGroup.removeFromParent() diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index 132179da3..9afcfeb3d 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -1,6 +1,7 @@ import { Vec3 } from 'vec3' import { World } from './world' import { getSectionGeometry, setBlockStatesData as setMesherData } from './models' +import { BlockStateModelInfo } from './shared' globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value)) @@ -182,6 +183,21 @@ setInterval(() => { } dirtySections.delete(key) } + + // Send new block state model info if any + if (world.blockStateModelInfo.size > 0) { + const newBlockStateInfo: Record = {} + for (const [cacheKey, info] of world.blockStateModelInfo) { + if (!world.sentBlockStateModels.has(cacheKey)) { + newBlockStateInfo[cacheKey] = info + world.sentBlockStateModels.add(cacheKey) + } + } + if (Object.keys(newBlockStateInfo).length > 0) { + postMessage({ type: 'blockStateModelInfo', info: newBlockStateInfo }) + } + } + // const time = performance.now() - start // console.log(`Processed ${sections.length} sections in ${time} ms (${time / sections.length} ms/section)`) }, 50) diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index 0dec40159..dd2952fb1 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -43,10 +43,6 @@ function prepareTints (tints) { }) } -function mod (x: number, n: number) { - return ((x % n) + n) % n -} - const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks) export function preflatBlockCalculation (block: Block, world: World, position: Vec3) { const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0] @@ -439,7 +435,16 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: } } -const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' +const ALWAYS_WATERLOGGED = new Set([ + 'seagrass', + 'tall_seagrass', + 'kelp', + 'kelp_plant', + 'bubble_column' +]) +const isBlockWaterlogged = (block: Block) => { + return block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' || ALWAYS_WATERLOGGED.has(block.name) +} let unknownBlockModel: BlockModelPartsResolved export function getSectionGeometry (sx, sy, sz, world: World) { diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index 6da1aed51..92ea2c82e 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -44,3 +44,14 @@ export type MesherGeometryOutput = { } export type HighestBlockInfo = { y: number, stateId: number | undefined, biomeId: number | undefined } + +export type BlockStateModelInfo = { + cacheKey: string + issues: string[] + modelNames: string[] + conditions: string[] +} + +export const getBlockAssetsCacheKey = (stateId: number, modelNameOverride?: string) => { + return modelNameOverride ? `${stateId}:${modelNameOverride}` : String(stateId) +} diff --git a/renderer/viewer/lib/mesher/world.ts b/renderer/viewer/lib/mesher/world.ts index c2dd3b7f1..f2757ae62 100644 --- a/renderer/viewer/lib/mesher/world.ts +++ b/renderer/viewer/lib/mesher/world.ts @@ -5,19 +5,11 @@ import { Vec3 } from 'vec3' import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json' import legacyJson from '../../../../src/preflatMap.json' -import { defaultMesherConfig, CustomBlockModels } from './shared' +import { defaultMesherConfig, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey } from './shared' import { INVISIBLE_BLOCKS } from './worldConstants' const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions) -const ALWAYS_WATERLOGGED = new Set([ - 'seagrass', - 'tall_seagrass', - 'kelp', - 'kelp_plant', - 'bubble_column' -]) - function columnKey (x, z) { return `${x},${z}` } @@ -39,7 +31,6 @@ export type WorldBlock = Omit & { _properties?: Record } - export class World { config = defaultMesherConfig Chunk: typeof import('prismarine-chunk/types/index').PCChunk @@ -49,6 +40,8 @@ export class World { preflat: boolean erroredBlockModel?: BlockModelPartsResolved customBlockModels = new Map() // chunkKey -> blockModels + sentBlockStateModels = new Set() + blockStateModelInfo = new Map() constructor (version) { this.Chunk = Chunks(version) as any @@ -123,7 +116,7 @@ export class World { return this.getColumn(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16) } - getBlock (pos: Vec3, blockProvider?, attr?): WorldBlock | null { + getBlock (pos: Vec3, blockProvider?: WorldBlockProvider, attr?: { hadErrors?: boolean }): WorldBlock | null { // for easier testing if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number]) const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16) @@ -138,7 +131,7 @@ export class World { const locInChunk = posInChunk(loc) const stateId = column.getBlockStateId(locInChunk) - const cacheKey = modelOverride ? `${stateId}:${modelOverride}` : stateId + const cacheKey = getBlockAssetsCacheKey(stateId, modelOverride) if (!this.blockCache[cacheKey]) { const b = column.getBlock(locInChunk) as unknown as WorldBlock @@ -171,17 +164,12 @@ export class World { } } - const block = this.blockCache[cacheKey] + const block: WorldBlock = this.blockCache[cacheKey] if (block.models === undefined && blockProvider) { if (!attr) throw new Error('attr is required') const props = block.getProperties() - // Patch waterlogged property for ocean plants - if (ALWAYS_WATERLOGGED.has(block.name)) { - props.waterlogged = 'true' - } - try { // fixme if (this.preflat) { @@ -196,17 +184,42 @@ export class World { } } - const useFallbackModel = this.preflat || modelOverride - block.models = blockProvider.getAllResolvedModels0_1({ - name: block.name, - properties: props, - }, useFallbackModel)! // fixme! this is a hack (also need a setting for all versions) - if (!block.models!.length) { + const useFallbackModel = !!(this.preflat || modelOverride) + const issues = [] as string[] + const resolvedModelNames = [] as string[] + const resolvedConditions = [] as string[] + block.models = blockProvider.getAllResolvedModels0_1( + { + name: block.name, + properties: props, + }, + useFallbackModel, + issues, + resolvedModelNames, + resolvedConditions + )! + + // Track block state model info + if (!this.sentBlockStateModels.has(cacheKey)) { + this.blockStateModelInfo.set(cacheKey, { + cacheKey, + issues, + modelNames: resolvedModelNames, + conditions: resolvedConditions + }) + } + + if (!block.models.length) { if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) { console.debug('[mesher] block to render not found', block.name, props) } block.models = null } + + if (block.models && modelOverride) { + const model = block.models[0] + block.transparent = model[0]?.['transparent'] ?? block.transparent + } } catch (err) { this.erroredBlockModel ??= blockProvider.getAllResolvedModels0_1({ name: 'errored', properties: {} }) block.models ??= this.erroredBlockModel @@ -219,6 +232,7 @@ export class World { if (block.name === 'flowing_lava') block.name = 'lava' if (block.name === 'bubble_column') block.name = 'water' // TODO need to distinguish between water and bubble column // block.position = loc // it overrides position of all currently loaded blocks + //@ts-expect-error block.biome = this.biomeCache[column.getBiome(locInChunk)] ?? this.biomeCache[1] ?? this.biomeCache[0] if (block.name === 'redstone_ore') block.transparent = false return block diff --git a/renderer/viewer/lib/smoothSwitcher.ts b/renderer/viewer/lib/smoothSwitcher.ts index 925affec6..74eb11717 100644 --- a/renderer/viewer/lib/smoothSwitcher.ts +++ b/renderer/viewer/lib/smoothSwitcher.ts @@ -88,8 +88,7 @@ export class SmoothSwitcher { onCancelled?: () => void ): void { if (this.isTransitioning) { - onCancelled?.() - this.animationController.forceFinish() + this.animationController.forceFinish(false) } this.transitioningToStateName = stateName ?? '' @@ -116,7 +115,7 @@ export class SmoothSwitcher { }) .start() return group - }) + }, onCancelled) } /** diff --git a/renderer/viewer/lib/ui/newStats.ts b/renderer/viewer/lib/ui/newStats.ts index 6fccac533..fff4d28c2 100644 --- a/renderer/viewer/lib/ui/newStats.ts +++ b/renderer/viewer/lib/ui/newStats.ts @@ -15,7 +15,7 @@ export const addNewStat = (id: string, width = 80, x = rightOffset, y = lastY) = pane.style.padding = '2px' pane.style.fontFamily = 'monospace' pane.style.fontSize = '12px' - pane.style.zIndex = '10000' + pane.style.zIndex = '100' pane.style.pointerEvents = 'none' document.body.appendChild(pane) stats[id] = pane diff --git a/renderer/viewer/lib/viewer.ts b/renderer/viewer/lib/viewer.ts index 7b951438a..941f21828 100644 --- a/renderer/viewer/lib/viewer.ts +++ b/renderer/viewer/lib/viewer.ts @@ -86,12 +86,7 @@ export class Viewer { console.log('[viewer] Using version:', userVersion, 'textures:', texturesVersion) this.entities.clear() // this.primitives.clear() - return this.world.setVersion(userVersion, texturesVersion).then(async () => { - return new THREE.TextureLoader().loadAsync(this.world.itemsAtlasParser!.latestImage) - }).then((texture) => { - this.entities.itemsTexture = texture - this.world.renderUpdateEmitter.emit('itemsTextureDownloaded') - }) + return this.world.setVersion(userVersion, texturesVersion) } addColumn (x, z, chunk, isLightUpdate = false) { diff --git a/renderer/viewer/lib/viewerWrapper.ts b/renderer/viewer/lib/viewerWrapper.ts index 2e08da1d6..6b2fb5625 100644 --- a/renderer/viewer/lib/viewerWrapper.ts +++ b/renderer/viewer/lib/viewerWrapper.ts @@ -1,5 +1,6 @@ import * as THREE from 'three' import { statsEnd, statsStart } from '../../../src/topRightStats' +import { activeModalStack } from '../../../src/globalState' // wrapper for now export class ViewerWrapper { @@ -77,8 +78,8 @@ export class ViewerWrapper { postRender = () => { } render (time: DOMHighResTimeStamp) { if (this.globalObject.stopLoop) return - for (const fn of beforeRenderFrame) fn() this.globalObject.requestAnimationFrame(this.render.bind(this)) + if (activeModalStack.some(m => m.reactType === 'app-status')) return if (!viewer || this.globalObject.stopRender || this.renderer?.xr.isPresenting || (this.stopRenderOnBlur && !this.windowFocused)) return const renderInterval = (this.windowFocused ? this.renderInterval : this.renderIntervalUnfocused) ?? this.renderInterval if (renderInterval) { @@ -91,6 +92,7 @@ export class ViewerWrapper { return } } + for (const fn of beforeRenderFrame) fn() this.preRender() statsStart() // ios bug: viewport dimensions are updated after the resize event diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 0cd0b9555..261b18e62 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -19,11 +19,12 @@ import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBloc import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs' import { toMajorVersion } from '../../../src/utils' import { buildCleanupDecorator } from './cleanupDecorator' -import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput, CustomBlockModels } from './mesher/shared' +import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo } from './mesher/shared' import { chunkPos } from './simpleUtils' import { HandItemBlock } from './holdingBlock' import { updateStatText } from './ui/newStats' import { WorldRendererThree } from './worldrendererThree' +import { generateGuiAtlas } from './guiRenderer' function mod (x, n) { return ((x % n) + n) % n @@ -149,10 +150,21 @@ export abstract class WorldRendererCommon lastChunkDistance = 0 debugStopGeometryUpdate = false + @worldCleanup() + freeFlyMode = false + @worldCleanup() + freeFlyState = { + yaw: 0, + pitch: 0, + position: new Vec3(0, 0, 0) + } @worldCleanup() itemsRenderer: ItemsRenderer | undefined - customBlockModels = new Map() + protocolCustomBlocks = new Map() + + @worldCleanup() + blockStateModelInfo = new Map() abstract outputFormat: 'threeJs' | 'webgpu' worldBlockProvider: WorldBlockProvider @@ -232,6 +244,12 @@ export abstract class WorldRendererCommon this.maxWorkersProcessTime = Math.max(this.maxWorkersProcessTime, data.processTime) } } + + if (data.type === 'blockStateModelInfo') { + for (const [cacheKey, info] of Object.entries(data.info)) { + this.blockStateModelInfo.set(cacheKey, info) + } + } } worker.onmessage = ({ data }) => { if (Array.isArray(data)) { @@ -337,6 +355,10 @@ export abstract class WorldRendererCommon } } + async generateGuiTextures () { + await generateGuiAtlas() + } + async updateAssetsData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) { const blocksAssetsParser = new AtlasParser(this.sourceData.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy) const itemsAssetsParser = new AtlasParser(this.sourceData.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy) @@ -348,15 +370,21 @@ export abstract class WorldRendererCommon } const customBlockTextures = Object.keys(this.customTextures.blocks?.textures ?? {}) + const customItemTextures = Object.keys(this.customTextures.items?.textures ?? {}) + console.time('createBlocksAtlas') const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => { const texture = this.customTextures?.blocks?.textures[textureName] return blockTexturesChanges[textureName] ?? texture }, /* this.customTextures?.blocks?.tileSize */undefined, prioritizeBlockTextures, customBlockTextures) + console.timeEnd('createBlocksAtlas') + console.time('createItemsAtlas') const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => { const texture = this.customTextures?.items?.textures[textureName] if (!texture) return return texture - }, this.customTextures?.items?.tileSize) + }, this.customTextures?.items?.tileSize, undefined, customItemTextures) + console.timeEnd('createItemsAtlas') + this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL()) this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL()) @@ -396,8 +424,31 @@ export abstract class WorldRendererCommon config: this.mesherConfig, }) } + if (!this.itemsAtlasParser) return + const itemsTexture = await new THREE.TextureLoader().loadAsync(this.itemsAtlasParser.latestImage) + itemsTexture.magFilter = THREE.NearestFilter + itemsTexture.minFilter = THREE.NearestFilter + itemsTexture.flipY = false + viewer.entities.itemsTexture = itemsTexture + if (!this.itemsAtlasParser) return + this.renderUpdateEmitter.emit('textureDownloaded') - console.log('texture loaded') + + console.time('generateGuiTextures') + await this.generateGuiTextures() + console.timeEnd('generateGuiTextures') + if (!this.itemsAtlasParser) return + this.renderUpdateEmitter.emit('itemsTextureDownloaded') + console.log('textures loaded') + } + + async downloadDebugAtlas (isItems = false) { + const atlasParser = (isItems ? this.itemsAtlasParser : this.blocksAtlasParser)! + const dataUrl = await atlasParser.createDebugImage(true) + const a = document.createElement('a') + a.href = dataUrl + a.download = `atlas-debug-${isItems ? 'items' : 'blocks'}.png` + a.click() } get worldMinYRender () { @@ -417,7 +468,7 @@ export abstract class WorldRendererCommon this.updateChunksStatsText() const chunkKey = `${x},${z}` - const customBlockModels = this.customBlockModels.get(chunkKey) + const customBlockModels = this.protocolCustomBlocks.get(chunkKey) for (const worker of this.workers) { worker.postMessage({ @@ -478,7 +529,7 @@ export abstract class WorldRendererCommon const needAoRecalculation = true const chunkKey = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}` const blockPosKey = `${pos.x},${pos.y},${pos.z}` - const customBlockModels = this.customBlockModels.get(chunkKey) || {} + const customBlockModels = this.protocolCustomBlocks.get(chunkKey) || {} for (const worker of this.workers) { worker.postMessage({ diff --git a/renderer/viewer/lib/worldrendererThree.ts b/renderer/viewer/lib/worldrendererThree.ts index 4ff56cdb9..697717c93 100644 --- a/renderer/viewer/lib/worldrendererThree.ts +++ b/renderer/viewer/lib/worldrendererThree.ts @@ -223,8 +223,15 @@ export class WorldRendererThree extends WorldRendererCommon { } updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void { + if (this.freeFlyMode) { + pos = this.freeFlyState.position + pitch = this.freeFlyState.pitch + yaw = this.freeFlyState.yaw + } + if (pos) { new tweenJs.Tween(this.camera.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start() + this.freeFlyState.position = pos } this.camera.rotation.set(pitch, yaw, this.cameraRoll, 'ZYX') } @@ -234,7 +241,7 @@ export class WorldRendererThree extends WorldRendererCommon { // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera this.renderer.render(this.scene, cam) - if (this.config.showHand) { + if (this.config.showHand && !this.freeFlyMode) { this.holdingBlock.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight) this.holdingBlockLeft.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight) } diff --git a/rsbuild.config.ts b/rsbuild.config.ts index a93e2c842..875d0e0c7 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -25,14 +25,26 @@ const disableServiceWorker = process.env.DISABLE_SERVICE_WORKER === 'true' let releaseTag let releaseLink let releaseChangelog +let githubRepositoryFallback if (fs.existsSync('./assets/release.json')) { const releaseJson = JSON.parse(fs.readFileSync('./assets/release.json', 'utf8')) releaseTag = releaseJson.latestTag releaseLink = releaseJson.isCommit ? `/commit/${releaseJson.latestTag}` : `/releases/${releaseJson.latestTag}` releaseChangelog = releaseJson.changelog?.replace(//, '') + githubRepositoryFallback = releaseJson.repository } +const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8')) +try { + Object.assign(configJson, JSON.parse(fs.readFileSync('./config.local.json', 'utf8'))) +} catch (err) {} +if (dev) { + configJson.defaultProxy = ':8080' +} + +const configSource = process.env.CONFIG_JSON_SOURCE || 'REMOTE' + // base options are in ./renderer/rsbuildSharedConfig.ts const appConfig = defineConfig({ html: { @@ -58,12 +70,13 @@ const appConfig = defineConfig({ 'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'), 'process.env.MAIN_MENU_LINKS': JSON.stringify(process.env.MAIN_MENU_LINKS), 'process.env.GITHUB_URL': - JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`), + JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}` || githubRepositoryFallback}`), 'process.env.DEPS_VERSIONS': JSON.stringify({}), 'process.env.RELEASE_TAG': JSON.stringify(releaseTag), 'process.env.RELEASE_LINK': JSON.stringify(releaseLink), 'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog), 'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker), + 'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null), }, }, server: { @@ -99,15 +112,10 @@ const appConfig = defineConfig({ if (fs.existsSync('./assets/release.json')) { fs.copyFileSync('./assets/release.json', './dist/release.json') } - const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8')) - let configLocalJson = {} - try { - configLocalJson = JSON.parse(fs.readFileSync('./config.local.json', 'utf8')) - } catch (err) {} - if (dev) { - configJson.defaultProxy = ':8080' + + if (configSource === 'REMOTE') { + fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8') } - fs.writeFileSync('./dist/config.json', JSON.stringify({ ...configJson, ...configLocalJson }), 'utf8') if (fs.existsSync('./generated/sounds.js')) { fs.copyFileSync('./generated/sounds.js', './dist/sounds.js') } diff --git a/scripts/dockerPrepare.mjs b/scripts/dockerPrepare.mjs index 37e57d015..62a4f5e4e 100644 --- a/scripts/dockerPrepare.mjs +++ b/scripts/dockerPrepare.mjs @@ -4,9 +4,27 @@ import path from 'path' import { fileURLToPath } from 'url' import { execSync } from 'child_process' -// write release tag +// Get repository from git config +const getGitRepository = () => { + try { + const gitConfig = fs.readFileSync('.git/config', 'utf8') + const originUrlMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = .*?github\.com[:/](.*?)(\.git)?\n/m) + if (originUrlMatch) { + return originUrlMatch[1] + } + } catch (err) { + console.warn('Failed to read git repository from config:', err) + } + return null +} + +// write release tag and repository info const commitShort = execSync('git rev-parse --short HEAD').toString().trim() -fs.writeFileSync('./assets/release.json', JSON.stringify({ latestTag: `${commitShort} (docker)` }), 'utf8') +const repository = getGitRepository() +fs.writeFileSync('./assets/release.json', JSON.stringify({ + latestTag: `${commitShort} (docker)`, + repository +}), 'utf8') const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')) delete packageJson.optionalDependencies diff --git a/scripts/makeOptimizedMcData.mjs b/scripts/makeOptimizedMcData.mjs index ebc97b599..eae0444b9 100644 --- a/scripts/makeOptimizedMcData.mjs +++ b/scripts/makeOptimizedMcData.mjs @@ -138,9 +138,9 @@ const dataTypeBundling = { protocol: { raw: true }, - sounds: { - arrKey: 'name' - } + // sounds: { + // arrKey: 'name' + // } } const notBundling = [...dataTypes.keys()].filter(x => !Object.keys(dataTypeBundling).includes(x)) diff --git a/scripts/prepareSounds.mjs b/scripts/prepareSounds.mjs index f9b8cd609..7ff614a29 100644 --- a/scripts/prepareSounds.mjs +++ b/scripts/prepareSounds.mjs @@ -7,10 +7,11 @@ import { fileURLToPath } from 'url' import { exec } from 'child_process' import { promisify } from 'util' import { build } from 'esbuild' +import supportedVersions from '../src/supportedVersions.mjs' const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url))) -const targetedVersions = ['1.21.1', '1.20.6', '1.20.1', '1.19.2', '1.18.2', '1.17.1', '1.16.5', '1.15.2', '1.14.4', '1.13.2', '1.12.2', '1.11.2', '1.10.2', '1.9.4', '1.8.9'] +const targetedVersions = supportedVersions.reverse() /** @type {{name, size, hash}[]} */ let prevSounds = null @@ -145,6 +146,7 @@ const convertSounds = async () => { const convertSound = async (i) => { const proc = promisify(exec)(`${ffmpegExec} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`) // pipe stdout to the console + //@ts-ignore proc.child.stdout.pipe(process.stdout) await proc console.log('converted to mp3', i, '/', toConvert.length, toConvert[i]) @@ -173,7 +175,11 @@ const writeSoundsMap = async () => { // const localTargetedVersions = targetedVersions.slice(0, 2) const localTargetedVersions = targetedVersions for (const targetedVersion of localTargetedVersions) { - const burgerData = await fetch(burgerDataUrl(targetedVersion)).then((r) => r.json()) + const burgerData = await fetch(burgerDataUrl(targetedVersion)).then((r) => r.json()).catch((err) => { + console.error('error fetching burger data', targetedVersion, err) + return null + }) + if (!burgerData) continue const allSoundsMap = getSoundsMap(burgerData) // console.log(Object.keys(sounds).length, 'ids') const outputIdMap = {} @@ -240,6 +246,7 @@ const writeSoundsMap = async () => { const makeSoundsBundle = async () => { const allSoundsMap = JSON.parse(fs.readFileSync('./generated/sounds.json', 'utf8')) const allSoundsVersionedMap = JSON.parse(fs.readFileSync('./generated/soundsPathVersionsRemap.json', 'utf8')) + if (!process.env.REPO_SLUG) throw new Error('REPO_SLUG is not set') const allSoundsMeta = { format: 'mp3', diff --git a/server.js b/server.js index 2dbb05b36..20e66051f 100644 --- a/server.js +++ b/server.js @@ -15,7 +15,7 @@ try { // Create our app const app = express() -const isProd = process.argv.includes('--prod') +const isProd = process.argv.includes('--prod') || process.env.NODE_ENV === 'production' app.use(compression()) app.use(cors()) app.use(netApi({ allowOrigin: '*' })) diff --git a/src/appConfig.ts b/src/appConfig.ts new file mode 100644 index 000000000..3d6d8f93a --- /dev/null +++ b/src/appConfig.ts @@ -0,0 +1,59 @@ +import { disabledSettings, options, qsOptions } from './optionsStorage' +import { miscUiState } from './globalState' +import { setLoadingScreenStatus } from './appStatus' + +export type AppConfig = { + // defaultHost?: string + // defaultHostSave?: string + defaultProxy?: string + // defaultProxySave?: string + // defaultVersion?: string + peerJsServer?: string + peerJsServerFallback?: string + promoteServers?: Array<{ ip, description, version? }> + mapsProvider?: string + + appParams?: Record // query string params + + defaultSettings?: Record + forceSettings?: Record + // hideSettings?: Record + allowAutoConnect?: boolean + pauseLinks?: Array>> +} + +export const loadAppConfig = (appConfig: AppConfig) => { + if (miscUiState.appConfig) { + Object.assign(miscUiState.appConfig, appConfig) + } else { + miscUiState.appConfig = appConfig + } + + if (appConfig.forceSettings) { + for (const [key, value] of Object.entries(appConfig.forceSettings)) { + if (value) { + disabledSettings.value.add(key) + // since the setting is forced, we need to set it to that value + if (appConfig.defaultSettings?.[key] && !qsOptions[key]) { + options[key] = appConfig.defaultSettings[key] + } + } else { + disabledSettings.value.delete(key) + } + } + } +} + +export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG + +if (isBundledConfigUsed) { + loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {}) +} else { + void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { + // console.warn('Failed to load optional app config.json', error) + // return {} + setLoadingScreenStatus('Failed to load app config.json', true) + }).then((config: AppConfig) => { + loadAppConfig(config) + }) +} diff --git a/src/appParams.ts b/src/appParams.ts index 61ee9e072..98d6ff620 100644 --- a/src/appParams.ts +++ b/src/appParams.ts @@ -1,3 +1,5 @@ +import type { AppConfig } from './appConfig' + const qsParams = new URLSearchParams(window.location?.search ?? '') export type AppQsParams = { @@ -39,6 +41,15 @@ export type AppQsParams = { suggest_save?: string noPacketsValidation?: string testCrashApp?: string + + // Replay params + replayFilter?: string + replaySpeed?: string + replayFileUrl?: string + replayValidateClient?: string + replayStopOnError?: string + replaySkipMissingOnTimeout?: string + replayPacketsSenderDelay?: string } export type AppQsParamsArray = { @@ -52,12 +63,17 @@ type AppQsParamsArrayTransformed = { [k in keyof AppQsParamsArray]: string[] } +globalThis.process ??= {} as any +const initialAppConfig = process?.env?.INLINED_APP_CONFIG as AppConfig ?? {} + export const appQueryParams = new Proxy({} as AppQsParams, { get (target, property) { if (typeof property !== 'string') { - return null + return undefined } - return qsParams.get(property) + const qsParam = qsParams.get(property) + if (qsParam) return qsParam + return initialAppConfig.appParams?.[property] }, }) @@ -66,10 +82,22 @@ export const appQueryParamsArray = new Proxy({} as AppQsParamsArrayTransformed, if (typeof property !== 'string') { return null } - return qsParams.getAll(property) + const qsParam = qsParams.getAll(property) + if (qsParam.length) return qsParam + return initialAppConfig.appParams?.[property] ?? [] }, }) +export function updateQsParam (name: keyof AppQsParams, value: string | undefined) { + const url = new URL(window.location.href) + if (value) { + url.searchParams.set(name, value) + } else { + url.searchParams.delete(name) + } + window.history.replaceState({}, '', url.toString()) +} + // Helper function to check if a specific query parameter exists export const hasQueryParam = (param: keyof AppQsParams) => qsParams.has(param) diff --git a/src/appStatus.ts b/src/appStatus.ts index 4c82973a4..d3bfc4616 100644 --- a/src/appStatus.ts +++ b/src/appStatus.ts @@ -33,3 +33,4 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul appStatusState.status = status appStatusState.minecraftJsonMessage = minecraftJsonMessage ?? null } +globalThis.setLoadingScreenStatus = setLoadingScreenStatus diff --git a/src/basicSounds.ts b/src/basicSounds.ts index 6c2b5f4ff..40428c6b8 100644 --- a/src/basicSounds.ts +++ b/src/basicSounds.ts @@ -17,17 +17,30 @@ const convertedSounds = [] as string[] export async function loadSound (path: string, contents = path) { if (loadingSounds.includes(path)) return true loadingSounds.push(path) - const res = await window.fetch(contents) - if (!res.ok) { - const error = `Failed to load sound ${path}` - if (isCypress()) throw new Error(error) - else console.warn(error) - return - } - const data = await res.arrayBuffer() - sounds[path] = data - loadingSounds.splice(loadingSounds.indexOf(path), 1) + try { + audioContext ??= new window.AudioContext() + + const res = await window.fetch(contents) + if (!res.ok) { + const error = `Failed to load sound ${path}` + if (isCypress()) throw new Error(error) + else console.warn(error) + return + } + const arrayBuffer = await res.arrayBuffer() + + // Decode the audio data immediately + const audioBuffer = await audioContext.decodeAudioData(arrayBuffer) + sounds[path] = audioBuffer + convertedSounds.push(path) // Mark as converted immediately + + loadingSounds.splice(loadingSounds.indexOf(path), 1) + } catch (err) { + console.warn(`Failed to load sound ${path}:`, err) + loadingSounds.splice(loadingSounds.indexOf(path), 1) + if (isCypress()) throw err + } } export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) => { @@ -53,13 +66,6 @@ export async function playSound (url, soundVolume = 1) { return } - for (const [soundName, sound] of Object.entries(sounds)) { - if (convertedSounds.includes(soundName)) continue - // eslint-disable-next-line no-await-in-loop - sounds[soundName] = await audioContext.decodeAudioData(sound) - convertedSounds.push(soundName) - } - const soundBuffer = sounds[url] if (!soundBuffer) { console.warn(`Sound ${url} not loaded yet`) diff --git a/src/browserfs.ts b/src/browserfs.ts index bd862ae0b..0f4579b8e 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -10,22 +10,45 @@ import { fsState, loadSave } from './loadSave' import { installResourcepackPack, installTexturePackFromHandle, updateTexturePackInstalledState } from './resourcePack' import { miscUiState } from './globalState' import { setLoadingScreenStatus } from './appStatus' -const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive') // disable type checking +import { VALID_REPLAY_EXTENSIONS, openFile } from './packetsReplay/replayPackets' +import { getFixedFilesize } from './downloadAndOpenFile' +import { packetsReplayState } from './react/state/packetsReplayState' +import { createFullScreenProgressReporter } from './core/progressReporter' +import { showNotification } from './react/NotificationProvider' +const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive') browserfs.install(window) const defaultMountablePoints = { - '/world': { fs: 'LocalStorage' }, // will be removed in future '/data': { fs: 'IndexedDB' }, '/resourcepack': { fs: 'InMemory' }, // temporary storage for currently loaded resource pack + '/temp': { fs: 'InMemory' } +} +const fallbackMountablePoints = { + '/resourcepack': { fs: 'InMemory' }, // temporary storage for downloaded server resource pack + '/temp': { fs: 'InMemory' } } browserfs.configure({ fs: 'MountableFileSystem', options: defaultMountablePoints, }, async (e) => { - // todo disable singleplayer button - if (e) throw e + if (e) { + browserfs.configure({ + fs: 'MountableFileSystem', + options: fallbackMountablePoints, + }, async (e2) => { + if (e2) { + showNotification('Unknown FS error, cannot continue', e2.message, true) + throw e2 + } + showNotification('Failed to access device storage', `Check you have free space. ${e.message}`, true) + miscUiState.fsReady = true + miscUiState.singleplayerAvailable = false + }) + return + } await updateTexturePackInstalledState() - miscUiState.appLoaded = true + miscUiState.fsReady = true + miscUiState.singleplayerAvailable = true }) export const forceCachedDataPaths = {} @@ -621,22 +644,33 @@ export const openFilePicker = (specificCase?: 'resourcepack') => { if (!picker) { picker = document.createElement('input') picker.type = 'file' - picker.accept = '.zip' + picker.accept = specificCase ? '.zip' : [...VALID_REPLAY_EXTENSIONS, '.zip'].join(',') picker.addEventListener('change', () => { const file = picker.files?.[0] picker.value = '' if (!file) return - if (!file.name.endsWith('.zip')) { - const doContinue = confirm(`Are you sure ${file.name.slice(-20)} is .zip file? Only .zip files are supported. Continue?`) - if (!doContinue) return - } if (specificCase === 'resourcepack') { - void installResourcepackPack(file).catch((err) => { + if (!file.name.endsWith('.zip')) { + const doContinue = confirm(`Are you sure ${file.name.slice(-20)} is .zip file? ONLY .zip files are supported. Continue?`) + if (!doContinue) return + } + void installResourcepackPack(file, createFullScreenProgressReporter()).catch((err) => { setLoadingScreenStatus(err.message, true) }) } else { - void openWorldZip(file) + // eslint-disable-next-line no-lonely-if + if (VALID_REPLAY_EXTENSIONS.some(ext => file.name.endsWith(ext)) || file.name.startsWith('packets-replay')) { + void file.text().then(contents => { + openFile({ + contents, + filename: file.name, + filesize: file.size + }) + }) + } else { + void openWorldZip(file) + } } }) picker.hidden = true diff --git a/src/cameraRotationControls.ts b/src/cameraRotationControls.ts index 31c206543..0c222dc6e 100644 --- a/src/cameraRotationControls.ts +++ b/src/cameraRotationControls.ts @@ -3,20 +3,10 @@ import { activeModalStack, isGameActive, miscUiState, showModal } from './global import { options } from './optionsStorage' import { hideNotification, notificationProxy } from './react/NotificationProvider' import { pointerLock } from './utils' -import worldInteractions from './worldInteractions' import { updateMotion, initMotionTracking } from './react/uiMotion' let lastMouseMove: number -const MOTION_DAMPING = 0.92 -const MAX_MOTION_OFFSET = 30 -const motionVelocity = { x: 0, y: 0 } -const lastUpdate = performance.now() - -export const updateCursor = () => { - worldInteractions.update() -} - export type CameraMoveEvent = { movementX: number movementY: number @@ -30,7 +20,7 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) { e.stopPropagation?.() const now = performance.now() // todo: limit camera movement for now to avoid unexpected jumps - if (now - lastMouseMove < 4) return + if (now - lastMouseMove < 4 && !options.preciseMouseInput) return lastMouseMove = now let { mouseSensX, mouseSensY } = options if (mouseSensY === -1) mouseSensY = mouseSensX @@ -38,7 +28,7 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) { x: e.movementX * mouseSensX * 0.0001, y: e.movementY * mouseSensY * 0.0001 }) - updateCursor() + bot.mouse.update() updateMotion() } @@ -48,6 +38,14 @@ export const moveCameraRawHandler = ({ x, y }: { x: number; y: number }) => { const minPitch = -0.5 * Math.PI viewer.world.lastCamUpdate = Date.now() + + if (viewer.world.freeFlyMode) { + // Update freeFlyState directly + viewer.world.freeFlyState.yaw = (viewer.world.freeFlyState.yaw - x) % (2 * Math.PI) + viewer.world.freeFlyState.pitch = Math.max(minPitch, Math.min(maxPitch, viewer.world.freeFlyState.pitch - y)) + return + } + if (!bot?.entity) return const pitch = bot.entity.pitch - y void bot.look(bot.entity.yaw - x, Math.max(minPitch, Math.min(maxPitch, pitch)), true) diff --git a/src/connect.ts b/src/connect.ts index 9ada9b8a8..a67e6a62c 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -23,6 +23,9 @@ export type ConnectOptions = { peerOptions?: any viewerWsConnect?: string saveServerToHistory?: boolean + + /** Will enable local replay server */ + worldStateFileContents?: string } export const getVersionAutoSelect = (autoVersionSelect = options.serversAutoVersionSelect) => { diff --git a/src/controls.ts b/src/controls.ts index b0add0432..a3a54ffb9 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -2,13 +2,14 @@ import { Vec3 } from 'vec3' import { proxy, subscribe } from 'valtio' +import * as THREE from 'three' import { ControMax } from 'contro-max/build/controMax' import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types' import { stringStartsWith } from 'contro-max/build/stringUtils' import { UserOverrideCommand, UserOverridesConfig } from 'contro-max/build/types/store' import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, hideModal, hideAllModals } from './globalState' -import { goFullscreen, pointerLock, reloadChunks } from './utils' +import { goFullscreen, isInRealGameSession, pointerLock, reloadChunks } from './utils' import { options } from './optionsStorage' import { openPlayerInventory } from './inventoryWindows' import { chatInputValueGlobal } from './react/Chat' @@ -19,10 +20,11 @@ import { showOptionsModal } from './react/SelectOption' import widgets from './react/widgets' import { getItemFromBlock } from './chatUtils' import { gamepadUiCursorState, moveGamepadCursorByPx } from './react/GamepadUiCursor' -import { completeTexturePackInstall, copyServerResourcePackToRegular, resourcePackState } from './resourcePack' +import { completeResourcepackPackInstall, copyServerResourcePackToRegular, resourcePackState } from './resourcePack' import { showNotification } from './react/NotificationProvider' import { lastConnectOptions } from './react/AppStatusProvider' import { onCameraMove, onControInit } from './cameraRotationControls' +import { createNotificationProgressReporter } from './core/progressReporter' export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) as UserOverridesConfig @@ -127,6 +129,23 @@ contro.on('movementUpdate', ({ vector, soleVector, gamepadIndex }) => { } miscUiState.usingGamepadInput = gamepadIndex !== undefined if (!bot || !isGameActive(false)) return + + if (viewer.world.freeFlyMode) { + // Create movement vector from input + const direction = new THREE.Vector3(0, 0, 0) + if (vector.z !== undefined) direction.z = vector.z + if (vector.x !== undefined) direction.x = vector.x + + // Apply camera rotation to movement direction + direction.applyQuaternion(viewer.camera.quaternion) + + // Update freeFlyState position with normalized direction + const moveSpeed = 1 + direction.multiplyScalar(moveSpeed) + viewer.world.freeFlyState.position.add(new Vec3(direction.x, direction.y, direction.z)) + return + } + // gamepadIndex will be used for splitscreen in future const coordToAction = [ ['z', -1, 'forward'], @@ -333,10 +352,20 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (command) { case 'general.jump': - bot.setControlState('jump', pressed) + if (viewer.world.freeFlyMode) { + const moveSpeed = 0.5 + viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? moveSpeed : 0, 0)) + } else { + bot.setControlState('jump', pressed) + } break case 'general.sneak': - setSneaking(pressed) + if (viewer.world.freeFlyMode) { + const moveSpeed = 0.5 + viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? -moveSpeed : 0, 0)) + } else { + setSneaking(pressed) + } break case 'general.sprint': // todo add setting to change behavior @@ -599,7 +628,7 @@ export const f3Keybinds: Array<{ // TODO! if (resourcePackState.resourcePackInstalled || gameAdditionalState.usingServerResourcePack) { showNotification('Reloading textures...') - await completeTexturePackInstall('default', 'default', gameAdditionalState.usingServerResourcePack) + await completeResourcepackPackInstall('default', 'default', gameAdditionalState.usingServerResourcePack, createNotificationProgressReporter()) } }, mobileTitle: 'Reload Textures' @@ -640,8 +669,8 @@ export const f3Keybinds: Array<{ const proxyPing = await bot['pingProxy']() void showOptionsModal(`${username}: last known total latency (ping): ${playerPing}. Connected to ${lastConnectOptions.value?.proxy} with current ping ${proxyPing}. Player UUID: ${uuid}`, []) }, - mobileTitle: 'Show Proxy & Ping Details', - enabled: () => !!lastConnectOptions.value?.proxy + mobileTitle: 'Show Player & Ping Details', + enabled: () => !lastConnectOptions.value?.singleplayer && !!bot.player }, { action () { @@ -835,7 +864,7 @@ const selectItem = async () => { addEventListener('mousedown', async (e) => { if ((e.target as HTMLElement).matches?.('#VRButton')) return - if (gameAdditionalState.viewerConnection && !(e.target as HTMLElement).id.includes('ui-root')) return + if (!isInRealGameSession() && !(e.target as HTMLElement).id.includes('ui-root')) return void pointerLock.requestPointerLock() if (!bot) return // wheel click diff --git a/src/core/progressReporter.ts b/src/core/progressReporter.ts new file mode 100644 index 000000000..6ef6044f3 --- /dev/null +++ b/src/core/progressReporter.ts @@ -0,0 +1,219 @@ +import { setLoadingScreenStatus } from '../appStatus' +import { appStatusState } from '../react/AppStatusProvider' +import { hideNotification, showNotification } from '../react/NotificationProvider' + +export interface ProgressReporter { + currentMessage: string | undefined + beginStage (stage: string, title: string): void + endStage (stage: string): void + setSubStage (stage: string, subStageTitle: string): void + reportProgress (stage: string, progress: number): void + executeWithMessage(message: string, fn: () => Promise): Promise + executeWithMessage(message: string, stage: string, fn: () => Promise): Promise + + setMessage (message: string): void + + end(): void + error(message: string): void +} + +interface ReporterDisplayImplementation { + setMessage (message: string): void + end (): void + error(message: string): void +} + +interface StageInfo { + title: string + subStage?: string + progress?: number +} + +const NO_STAGES_ACTION_END = false + +const createProgressReporter = (implementation: ReporterDisplayImplementation): ProgressReporter => { + const stages = new Map() + let currentMessage: string | undefined + let ended = false + + const end = () => { + if (ended) return + ended = true + stages.clear() + implementation.end() + } + + const updateStatus = () => { + if (ended) return + const activeStages = [...stages.entries()] + if (activeStages.length === 0) { + if (NO_STAGES_ACTION_END) { + end() + } else { + implementation.setMessage('Waiting for tasks') + } + return + } + + const [currentStage, info] = activeStages.at(-1)! + let message = info.title + if (info.subStage) { + message += ` - ${info.subStage}` + } + if (info.progress !== undefined) { + const num = Math.round(info.progress * 100) + if (isFinite(num)) { + message += `: ${num}%` + } + } + + currentMessage = message + implementation.setMessage(message) + } + + const reporter = { + beginStage (stage: string, title: string) { + if (stages.has(stage)) { + throw new Error(`Stage ${stage} already is running`) + } + stages.set(stage, { title }) + updateStatus() + }, + + endStage (stage: string) { + stages.delete(stage) + updateStatus() + }, + + setSubStage (stage: string, subStageTitle: string) { + const info = stages.get(stage) + if (info) { + info.subStage = subStageTitle + updateStatus() + } + }, + + reportProgress (stage: string, progress: number) { + const info = stages.get(stage) + if (info) { + info.progress = progress + updateStatus() + } + }, + + async executeWithMessage(...args: any[]): Promise { + const message = args[0] + const stage = typeof args[1] === 'string' ? args[1] : undefined + const fn = typeof args[1] === 'string' ? args[2] : args[1] + + const tempStage = stage ?? 'temp-' + Math.random().toString(36).slice(2) + reporter.beginStage(tempStage, message) + try { + const result = await fn() + return result + } finally { + reporter.endStage(tempStage) + } + }, + + end (): void { + end() + }, + + setMessage (message: string): void { + implementation.setMessage(message) + }, + + get currentMessage () { + return currentMessage + }, + + error (message: string): void { + implementation.error(message) + } + } + + return reporter +} + +const fullScreenReporters = [] as ProgressReporter[] +export const createFullScreenProgressReporter = (): ProgressReporter => { + const reporter = createProgressReporter({ + setMessage (message: string) { + if (appStatusState.isError) return + setLoadingScreenStatus(message) + }, + end () { + if (appStatusState.isError) return + fullScreenReporters.splice(fullScreenReporters.indexOf(reporter), 1) + if (fullScreenReporters.length === 0) { + setLoadingScreenStatus(undefined) + } else { + setLoadingScreenStatus(fullScreenReporters.at(-1)!.currentMessage) + } + }, + + error (message: string): void { + if (appStatusState.isError) return + setLoadingScreenStatus(message, true) + } + }) + fullScreenReporters.push(reporter) + return reporter +} + +export const createNotificationProgressReporter = (endMessage?: string): ProgressReporter => { + return createProgressReporter({ + setMessage (message: string) { + showNotification(`${message}...`, '', false, '', undefined, true) + }, + end () { + if (endMessage) { + showNotification(endMessage, '', false, '', undefined, true) + } else { + hideNotification() + } + }, + + error (message: string): void { + showNotification(message, '', true, '', undefined, true) + } + }) +} + +export const createConsoleLogProgressReporter = (): ProgressReporter => { + return createProgressReporter({ + setMessage (message: string) { + console.log(message) + }, + end () { + console.log('done') + }, + + error (message: string): void { + console.error(message) + } + }) +} + +export const createWrappedProgressReporter = (reporter: ProgressReporter, message?: string) => { + const stage = `wrapped-${message}` + if (message) { + reporter.beginStage(stage, message) + } + + return createProgressReporter({ + setMessage (message: string) { + reporter.setMessage(message) + }, + end () { + if (message) { + reporter.endStage(stage) + } + }, + + error (message: string): void { + reporter.error(message) + } + }) +} diff --git a/src/customChannels.ts b/src/customChannels.ts index 075bf23a0..ff0f8a32d 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -8,7 +8,10 @@ customEvents.on('mineflayerBotCreated', async () => { resolve(true) }) }) + registerBlockModelsChannel() +}) +const registerBlockModelsChannel = () => { const CHANNEL_NAME = 'minecraft-web-client:blockmodels' const packetStructure = [ @@ -41,35 +44,66 @@ customEvents.on('mineflayerBotCreated', async () => { bot._client.on(CHANNEL_NAME as any, (data) => { const { worldName, x, y, z, model } = data - console.debug('Received model data:', { worldName, x, y, z, model }) - if (viewer?.world) { - const chunkX = Math.floor(x / 16) * 16 - const chunkZ = Math.floor(z / 16) * 16 - const chunkKey = `${chunkX},${chunkZ}` - const blockPosKey = `${x},${y},${z}` + const chunkX = Math.floor(x / 16) * 16 + const chunkZ = Math.floor(z / 16) * 16 + const chunkKey = `${chunkX},${chunkZ}` + const blockPosKey = `${x},${y},${z}` - const chunkModels = viewer.world.customBlockModels.get(chunkKey) || {} + const chunkModels = viewer.world.protocolCustomBlocks.get(chunkKey) || {} - if (model) { - chunkModels[blockPosKey] = model - } else { - delete chunkModels[blockPosKey] - } + if (model) { + chunkModels[blockPosKey] = model + } else { + delete chunkModels[blockPosKey] + } - if (Object.keys(chunkModels).length > 0) { - viewer.world.customBlockModels.set(chunkKey, chunkModels) - } else { - viewer.world.customBlockModels.delete(chunkKey) - } + if (Object.keys(chunkModels).length > 0) { + viewer.world.protocolCustomBlocks.set(chunkKey, chunkModels) + } else { + viewer.world.protocolCustomBlocks.delete(chunkKey) + } - // Trigger update - const block = worldView!.world.getBlock(new Vec3(x, y, z)) + // Trigger update + if (worldView) { + const block = worldView.world.getBlock(new Vec3(x, y, z)) if (block) { - worldView!.world.setBlockStateId(new Vec3(x, y, z), block.stateId) + worldView.world.setBlockStateId(new Vec3(x, y, z), block.stateId) } } + }) console.debug(`registered custom channel ${CHANNEL_NAME} channel`) -}) +} + +const registeredJeiChannel = () => { + const CHANNEL_NAME = 'minecraft-web-client:jei' + // id - string, categoryTitle - string, items - string (json array) + const packetStructure = [ + 'container', + [ + { + name: 'id', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'categoryTitle', + type: ['pstring', { countType: 'i16' }] + }, + { + name: 'items', + type: ['pstring', { countType: 'i16' }] + }, + ] + ] + + bot._client.registerChannel(CHANNEL_NAME, packetStructure, true) + + bot._client.on(CHANNEL_NAME as any, (data) => { + const { id, categoryTitle, items } = data + // ... + }) + + console.debug(`registered custom channel ${CHANNEL_NAME} channel`) +} diff --git a/src/devtools.ts b/src/devtools.ts index 5d0be20bf..617a4440f 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -3,7 +3,6 @@ import fs from 'fs' import { WorldRendererThree } from 'renderer/viewer/lib/worldrendererThree' import { enable, disable, enabled } from 'debug' -import { getEntityCursor } from './worldInteractions' window.cursorBlockRel = (x = 0, y = 0, z = 0) => { const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z) @@ -11,8 +10,8 @@ window.cursorBlockRel = (x = 0, y = 0, z = 0) => { return bot.world.getBlock(newPos) } -window.cursorEntity = () => { - return getEntityCursor() +window.entityCursor = () => { + return bot.mouse.getCursorState().entity } // wanderer diff --git a/src/downloadAndOpenFile.ts b/src/downloadAndOpenFile.ts index b1cf6859a..870a70b11 100644 --- a/src/downloadAndOpenFile.ts +++ b/src/downloadAndOpenFile.ts @@ -3,12 +3,31 @@ import { openWorldFromHttpDir, openWorldZip } from './browserfs' import { getResourcePackNames, installResourcepackPack, resourcePackState, updateTexturePackInstalledState } from './resourcePack' import { setLoadingScreenStatus } from './appStatus' import { appQueryParams, appQueryParamsArray } from './appParams' +import { VALID_REPLAY_EXTENSIONS, openFile } from './packetsReplay/replayPackets' +import { createFullScreenProgressReporter } from './core/progressReporter' export const getFixedFilesize = (bytes: number) => { return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } const inner = async () => { + const { replayFileUrl } = appQueryParams + if (replayFileUrl) { + setLoadingScreenStatus('Downloading replay file...') + const response = await fetch(replayFileUrl) + const contentLength = response.headers?.get('Content-Length') + const size = contentLength ? +contentLength : undefined + const filename = replayFileUrl.split('/').pop() + + const contents = await response.text() + openFile({ + contents, + filename, + filesize: size + }) + return true + } + const mapUrlDir = appQueryParamsArray.mapDir ?? [] const mapUrlDirGuess = appQueryParams.mapDirGuess const mapUrlDirBaseUrl = appQueryParams.mapDirBaseUrl @@ -74,7 +93,7 @@ const inner = async () => { })).arrayBuffer() if (texturepack) { const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-30) - await installResourcepackPack(buffer, name) + await installResourcepackPack(buffer, createFullScreenProgressReporter(), name) } else { await openWorldZip(buffer) } diff --git a/src/dragndrop.ts b/src/dragndrop.ts index 1f4b0e2b1..6be905517 100644 --- a/src/dragndrop.ts +++ b/src/dragndrop.ts @@ -6,6 +6,7 @@ import { versions } from 'minecraft-data' import { openWorldDirectory, openWorldZip } from './browserfs' import { isGameActive } from './globalState' import { showNotification } from './react/NotificationProvider' +import { openFile, VALID_REPLAY_EXTENSIONS } from './packetsReplay/replayPackets' const parseNbt = promisify(nbt.parse) const simplifyNbt = nbt.simplify @@ -53,10 +54,19 @@ async function handleDroppedFile (file: File) { alert('Rar files are not supported yet!') return } + if (VALID_REPLAY_EXTENSIONS.some(ext => file.name.endsWith(ext)) || file.name.startsWith('packets-replay')) { + const contents = await file.text() + openFile({ + contents, + filename: file.name, + filesize: file.size + }) + return + } if (file.name.endsWith('.mca')) { - const tempPath = '/data/temp.mca' + const tempPath = '/temp/temp.mca' try { - await fs.promises.writeFile(tempPath, Buffer.from(await file.arrayBuffer())) + await fs.promises.writeFile(tempPath, Buffer.from(await file.arrayBuffer()) as any) const region = new RegionFile(tempPath) await region.initialize() const chunks: Record = {} diff --git a/src/external/index.ts b/src/external/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/globalDomListeners.ts b/src/globalDomListeners.ts index 866c9784a..5055c6006 100644 --- a/src/globalDomListeners.ts +++ b/src/globalDomListeners.ts @@ -1,6 +1,7 @@ import { saveServer } from './flyingSquidUtils' import { isGameActive, activeModalStack } from './globalState' import { options } from './optionsStorage' +import { isInRealGameSession } from './utils' window.addEventListener('unload', (e) => { if (!window.justReloaded) { @@ -25,6 +26,7 @@ window.addEventListener('beforeunload', (event) => { if (!isGameActive(true) && activeModalStack.at(-1)?.elem?.id !== 'chat') return if (sessionStorage.lastReload && !options.preventDevReloadWhilePlaying) return if (!options.closeConfirmation) return + if (!isInRealGameSession()) return // For major browsers doning only this is enough event.preventDefault() diff --git a/src/globalState.ts b/src/globalState.ts index 412dd31bf..0ee8671d6 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -4,6 +4,8 @@ import { proxy, ref, subscribe } from 'valtio' import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps' import type { OptionsGroupType } from './optionsGuiScheme' import { appQueryParams } from './appParams' +import { options, disabledSettings } from './optionsStorage' +import { AppConfig } from './appConfig' // todo: refactor structure with support of hideNext=false @@ -109,21 +111,6 @@ export const showContextmenu = (items: ContextMenuItem[], { clientX, clientY }) // --- -export type AppConfig = { - // defaultHost?: string - // defaultHostSave?: string - defaultProxy?: string - // defaultProxySave?: string - // defaultVersion?: string - peerJsServer?: string - peerJsServerFallback?: string - promoteServers?: Array<{ ip, description, version? }> - mapsProvider?: string - - defaultSettings?: Record - allowAutoConnect?: boolean -} - export const miscUiState = proxy({ currentDisplayQr: null as string | null, currentTouch: null as boolean | null, @@ -138,7 +125,8 @@ export const miscUiState = proxy({ loadedServerIndex: '', /** currently trying to load or loaded mc version, after all data is loaded */ loadedDataVersion: null as string | null, - appLoaded: false, + fsReady: false, + singleplayerAvailable: false, usingGamepadInput: false, appConfig: null as AppConfig | null, displaySearchInput: false, diff --git a/src/globals.d.ts b/src/globals.d.ts index 96b32916f..6b2c66406 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -21,7 +21,7 @@ declare const loadedData: import('minecraft-data').IndexedData & { sounds: Recor declare const customEvents: import('typed-emitter').default<{ /** Singleplayer load requested */ singleplayer (): void - digStart () + digStart (): void gameLoaded (): void mineflayerBotCreated (): void search (q: string): void diff --git a/src/index.ts b/src/index.ts index 941b76c28..b87273951 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,9 +11,11 @@ import './mineflayer/maps' import './mineflayer/cameraShake' import './shims/patchShims' import './mineflayer/java-tester/index' +import './external' +import './appConfig' import { getServerInfo } from './mineflayer/mc-protocol' import { onGameLoad, renderSlot } from './inventoryWindows' -import { RenderItem } from './mineflayer/items' +import { GeneralInputItem, RenderItem } from './mineflayer/items' import initCollisionShapes from './getCollisionInteractionShapes' import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth' import microsoftAuthflow from './microsoftAuthflow' @@ -39,17 +41,14 @@ import { WorldDataEmitter, Viewer } from 'renderer/viewer' import pathfinder from 'mineflayer-pathfinder' import { Vec3 } from 'vec3' -import worldInteractions from './worldInteractions' - import * as THREE from 'three' import MinecraftData from 'minecraft-data' import debug from 'debug' import { defaultsDeep } from 'lodash-es' -import initializePacketsReplay from './packetsReplay' +import initializePacketsReplay from './packetsReplay/packetsReplayLegacy' import { initVR } from './vr' import { - AppConfig, activeModalStack, activeModalStacks, hideModal, @@ -57,7 +56,7 @@ import { isGameActive, miscUiState, showModal, - gameAdditionalState + gameAdditionalState, } from './globalState' import { parseServerAddress } from './parseServerAddress' @@ -96,7 +95,7 @@ import { ref, subscribe } from 'valtio' import { signInMessageState } from './react/SignInMessageProvider' import { updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage' import { versionToNumber } from 'renderer/viewer/prepare/utils' -import packetsPatcher from './packetsPatcher' +import packetsPatcher from './mineflayer/plugins/packetsPatcher' import { mainMenuState } from './react/MainMenuRenderApp' import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import './mobileShim' @@ -104,17 +103,20 @@ import { parseFormattedMessagePacket } from './botUtils' import { getViewerVersionData, getWsProtocolStream, handleCustomChannel } from './viewerConnector' import { getWebsocketStream } from './mineflayer/websocket-core' import { appQueryParams, appQueryParamsArray } from './appParams' -import { updateCursor } from './cameraRotationControls' -import { pingServerVersion } from './mineflayer/minecraft-protocol-extra' import { playerState, PlayerStateManager } from './mineflayer/playerState' import { states } from 'minecraft-protocol' import { initMotionTracking } from './react/uiMotion' import { UserError } from './mineflayer/userError' import ping from './mineflayer/plugins/ping' +import mouse from './mineflayer/plugins/mouse' +import { LocalServer } from './customServer' +import { startLocalReplayServer } from './packetsReplay/replayPackets' +import { localRelayServerPlugin } from './mineflayer/plugins/packetsRecording' +import { createFullScreenProgressReporter } from './core/progressReporter' +import { getItemModelName } from './resourcesManager' window.debug = debug window.THREE = THREE -window.worldInteractions = worldInteractions window.beforeRenderFrame = [] // ACTUAL CODE @@ -167,6 +169,11 @@ if (appQueryParams.testCrashApp === '2') throw new Error('test') // Create viewer const viewer: import('renderer/viewer/lib/viewer').Viewer = new Viewer(renderer, undefined, playerState) window.viewer = viewer +Object.defineProperty(window, 'world', { + get () { + return viewer.world + }, +}) // todo unify viewer.entities.getItemUv = (item, specificProps) => { const idOrName = item.itemId ?? item.blockId @@ -174,19 +181,13 @@ viewer.entities.getItemUv = (item, specificProps) => { const name = typeof idOrName === 'number' ? loadedData.items[idOrName]?.name : idOrName if (!name) throw new Error(`Item not found: ${idOrName}`) - const itemSelector = playerState.getItemSelector({ - ...specificProps - }) - const model = getItemDefinition(viewer.world.itemsDefinitionsStore, { + const model = getItemModelName({ + ...item, name, - version: viewer.world.texturesVersion!, - properties: itemSelector - })?.model ?? name + } as GeneralInputItem, specificProps) const renderInfo = renderSlot({ - ...item, - nbt: null, - name: model, + modelName: model, }, false, true) if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`) @@ -206,7 +207,8 @@ viewer.entities.getItemUv = (item, specificProps) => { const [u, v, su, sv] = [x / img.width, y / img.height, (w / img.width), (h / img.height)] return { u, v, su, sv, - texture: textureThree + texture: textureThree, + modelName: renderInfo.modelName } } @@ -309,8 +311,9 @@ export async function connect (connectOptions: ConnectOptions) { console.log('using player username', username) hideCurrentScreens() + const progress = createFullScreenProgressReporter() const loggingInMsg = connectOptions.server ? 'Connecting to server' : 'Logging in' - setLoadingScreenStatus(loggingInMsg) + progress.beginStage('connect', loggingInMsg) let ended = false let bot!: typeof __type_bot @@ -318,6 +321,7 @@ export async function connect (connectOptions: ConnectOptions) { if (ended) return ended = true viewer.resetAll() + progress.end() localServer = window.localServer = window.server = undefined gameAdditionalState.viewerConnection = false @@ -392,45 +396,61 @@ export async function connect (connectOptions: ConnectOptions) { const renderDistance = singleplayer ? renderDistanceSingleplayer : multiplayerRenderDistance let updateDataAfterJoin = () => { } let localServer + let localReplaySession: ReturnType | undefined try { const serverOptions = defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions) Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {}) - setLoadingScreenStatus('Downloading minecraft data') - await Promise.all([ - downloadAllMinecraftData(), // download mc data before we can use minecraft-data at all - downloadOtherGameData() - ]) - setLoadingScreenStatus(loggingInMsg) + + await progress.executeWithMessage('Downloading minecraft data', 'download-mcdata', async () => { + await Promise.all([ + downloadAllMinecraftData(), + downloadOtherGameData() + ]) + }) + let dataDownloaded = false const downloadMcData = async (version: string) => { if (dataDownloaded) return dataDownloaded = true - // if (connectOptions.authenticatedAccount && (versionToNumber(version) < versionToNumber('1.19.4') || versionToNumber(version) >= versionToNumber('1.21'))) { - // // todo support it (just need to fix .export crash) - // throw new UserError('Microsoft authentication is only supported on 1.19.4 - 1.20.6 (at least for now)') - // } - await downloadMcDataOnConnect(version) + await progress.executeWithMessage( + 'Applying user-installed resource pack', + async () => { + await downloadMcDataOnConnect(version) + try { + await resourcepackReload(version) + } catch (err) { + console.error(err) + const doContinue = confirm('Failed to apply texture pack. See errors in the console. Continue?') + if (!doContinue) { + throw err + } + } + } + ) + + await progress.executeWithMessage( + 'Loading minecraft models', + async () => { + viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json') + void viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures) + miscUiState.loadedDataVersion = version + } + ) + } + + let finalVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined) + + if (connectOptions.worldStateFileContents) { try { - // TODO! reload only after login packet (delay viewer display) so no unecessary reload after server one is isntalled - await resourcepackReload(version) + localReplaySession = startLocalReplayServer(connectOptions.worldStateFileContents) } catch (err) { console.error(err) - const doContinue = confirm('Failed to apply texture pack. See errors in the console. Continue?') - if (!doContinue) { - throw err - } + throw new UserError(`Failed to start local replay server: ${err}`) } - const oldStatus = appStatusState.status - setLoadingScreenStatus('Loading minecraft assets') - viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json') - void viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures) - miscUiState.loadedDataVersion = version - setLoadingScreenStatus(oldStatus) + finalVersion = localReplaySession.version } - let finalVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined) - if (singleplayer) { // SINGLEPLAYER EXPLAINER: // Note 1: here we have custom sync communication between server Client (flying-squid) and game client (mineflayer) @@ -443,21 +463,24 @@ export async function connect (connectOptions: ConnectOptions) { // Client (class) of flying-squid (in server/login.js of mc-protocol): onLogin handler: skip most logic & go to loginClient() which assigns uuid and sends 'success' back to client (onLogin handler) and emits 'login' on the server (login.js in flying-squid handler) // flying-squid: 'login' -> player.login -> now sends 'login' event to the client (handled in many plugins in mineflayer) -> then 'update_health' is sent which emits 'spawn' in mineflayer - setLoadingScreenStatus('Starting local server') localServer = window.localServer = window.server = startLocalServer(serverOptions) // todo need just to call quit if started // loadingScreen.maybeRecoverable = false // init world, todo: do it for any async plugins if (!localServer.pluginsReady) { - await new Promise(resolve => { - localServer.once('pluginsReady', resolve) - }) + await progress.executeWithMessage( + 'Starting local server', + async () => { + await new Promise(resolve => { + localServer.once('pluginsReady', resolve) + }) + } + ) } localServer.on('newPlayer', (player) => { - // it's you! player.on('loadingStatus', (newStatus) => { - setLoadingScreenStatus(newStatus, false, false, true) + progress.setMessage(newStatus) }) }) flyingSquidEvents() @@ -476,9 +499,11 @@ export async function connect (connectOptions: ConnectOptions) { const autoVersionSelect = await getServerInfo(server.host, server.port ? Number(server.port) : undefined, versionAutoSelect) finalVersion = autoVersionSelect.version } - initialLoadingText = `Connecting to server ${server.host} with version ${finalVersion}` + initialLoadingText = `Connecting to server ${server.host}:${server.port ?? 25_565} with version ${finalVersion}` } else if (connectOptions.viewerWsConnect) { initialLoadingText = `Connecting to Mineflayer WebSocket server ${connectOptions.viewerWsConnect}` + } else if (connectOptions.worldStateFileContents) { + initialLoadingText = `Loading local replay server` } else { initialLoadingText = 'We have no idea what to do' } @@ -538,7 +563,7 @@ export async function connect (connectOptions: ConnectOptions) { ...clientDataStream ? { stream: clientDataStream as any, } : {}, - ...singleplayer || p2pMultiplayer ? { + ...singleplayer || p2pMultiplayer || localReplaySession ? { keepAlive: false, } : {}, ...singleplayer ? { @@ -546,6 +571,10 @@ export async function connect (connectOptions: ConnectOptions) { connect () { }, Client: CustomChannelClient as any, } : {}, + ...localReplaySession ? { + connect () { }, + Client: CustomChannelClient as any, + } : {}, onMsaCode (data) { signInMessageState.code = data.user_code signInMessageState.link = data.verification_uri @@ -608,15 +637,17 @@ export async function connect (connectOptions: ConnectOptions) { void handleCustomChannel() } customEvents.emit('mineflayerBotCreated') - if (singleplayer || p2pMultiplayer) { - // in case of p2pMultiplayer there is still flying-squid on the host side - const _supportFeature = bot.supportFeature - bot.supportFeature = ((feature) => { - if (unsupportedLocalServerFeatures.includes(feature)) { - return false - } - return _supportFeature(feature) - }) as typeof bot.supportFeature + if (singleplayer || p2pMultiplayer || localReplaySession) { + if (singleplayer || p2pMultiplayer) { + // in case of p2pMultiplayer there is still flying-squid on the host side + const _supportFeature = bot.supportFeature + bot.supportFeature = ((feature) => { + if (unsupportedLocalServerFeatures.includes(feature)) { + return false + } + return _supportFeature(feature) + }) as typeof bot.supportFeature + } bot.emit('inject_allowed') bot._client.emit('connect') @@ -661,11 +692,15 @@ export async function connect (connectOptions: ConnectOptions) { } catch (err) { handleError(err) } + if (!bot) return if (connectOptions.server) { bot.loadPlugin(ping) } - if (!bot) return + bot.loadPlugin(mouse) + if (!localReplaySession) { + bot.loadPlugin(localRelayServerPlugin) + } const p2pConnectTimeout = p2pMultiplayer ? setTimeout(() => { throw new UserError('Spawn timeout. There might be error on the other side, check console.') }, 20_000) : undefined @@ -711,8 +746,6 @@ export async function connect (connectOptions: ConnectOptions) { onBotCreate() bot.once('login', () => { - worldInteractions.initBot() - setLoadingScreenStatus('Loading world') const mcData = MinecraftData(bot.version) @@ -723,16 +756,23 @@ export async function connect (connectOptions: ConnectOptions) { window.pathfinder = pathfinder }) + const start = Date.now() + let worldWasReady = false + void viewer.world.renderUpdateEmitter.on('update', () => { + // todo might not emit as servers simply don't send chunk if it's empty + if (!viewer.world.allChunksFinished || worldWasReady) return + worldWasReady = true + console.log('All chunks done and ready! Time from renderer open to ready', (Date.now() - start) / 1000, 's') + viewer.render() // ensure the last state is rendered + document.dispatchEvent(new Event('cypress-world-ready')) + }) + const spawnEarlier = !singleplayer && !p2pMultiplayer // don't use spawn event, player can be dead bot.once(spawnEarlier ? 'forcedMove' : 'health', async () => { if (resourcePackState.isServerInstalling) { - setLoadingScreenStatus('Downloading resource pack') await new Promise(resolve => { subscribe(resourcePackState, () => { - if (!resourcePackState.isServerDownloading) { - setLoadingScreenStatus('Installing resource pack') - } if (!resourcePackState.isServerInstalling) { resolve() } @@ -742,15 +782,13 @@ export async function connect (connectOptions: ConnectOptions) { window.focus?.() errorAbortController.abort() - miscUiState.gameLoaded = true - miscUiState.loadedServerIndex = connectOptions.serverIndex ?? '' - customEvents.emit('gameLoaded') if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout) - playerState.onlineMode = !!connectOptions.authenticatedAccount setLoadingScreenStatus('Placing blocks (starting viewer)') - localStorage.lastConnectOptions = JSON.stringify(connectOptions) + if (!connectOptions.worldStateFileContents || connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) { + localStorage.lastConnectOptions = JSON.stringify(connectOptions) + } connectOptions.onSuccessfulPlay?.() if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && !Object.keys(window.debugQueryParams).length) { lockUrl() @@ -767,8 +805,6 @@ export async function connect (connectOptions: ConnectOptions) { const worldView = window.worldView = new WorldDataEmitter(bot.world, renderDistance, center) watchOptionsAfterWorldViewInit() - bot.on('physicsTick', () => updateCursor()) - void initVR() initMotionTracking() @@ -798,6 +834,36 @@ export async function connect (connectOptions: ConnectOptions) { onGameLoad(() => {}) if (appStatusState.isError) return + + const waitForChunks = async () => { + const waitForChunks = options.waitForChunksRender === 'sp-only' ? !!singleplayer : options.waitForChunksRender + if (viewer.world.allChunksFinished || !waitForChunks) { + return + } + + await progress.executeWithMessage( + 'Loading chunks', + 'chunks', + async () => { + await new Promise(resolve => { + let wasFinished = false + void viewer.world.renderUpdateEmitter.on('update', () => { + if (wasFinished) return + if (viewer.world.allChunksFinished) { + wasFinished = true + resolve() + } else { + const perc = Math.round(Object.keys(viewer.world.finishedChunks).length / viewer.world.chunksLength * 100) + progress.reportProgress('chunks', perc / 100) + } + }) + }) + } + ) + } + + await waitForChunks() + setTimeout(() => { if (appQueryParams.suggest_save) { showNotification('Suggestion', 'Save the world to keep your progress!', false, undefined, async () => { @@ -811,24 +877,18 @@ export async function connect (connectOptions: ConnectOptions) { } }, 600) + miscUiState.gameLoaded = true + miscUiState.loadedServerIndex = connectOptions.serverIndex ?? '' + customEvents.emit('gameLoaded') setLoadingScreenStatus(undefined) - const start = Date.now() - let done = false - void viewer.world.renderUpdateEmitter.on('update', () => { - // todo might not emit as servers simply don't send chunk if it's empty - if (!viewer.world.allChunksFinished || done) return - done = true - console.log('All chunks done and ready! Time from renderer open to ready', (Date.now() - start) / 1000, 's') - viewer.render() // ensure the last state is rendered - document.dispatchEvent(new Event('cypress-world-ready')) - }) + progress.end() }) if (singleplayer && connectOptions.serverOverrides.worldFolder) { fsState.saveLoaded = true } - if (!connectOptions.ignoreQs) { + if (!connectOptions.ignoreQs || process.env.NODE_ENV === 'development') { // todo cleanup customEvents.on('gameLoaded', () => { const commands = appQueryParamsArray.command ?? [] @@ -843,8 +903,9 @@ export async function connect (connectOptions: ConnectOptions) { const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined listenGlobalEvents() -watchValue(miscUiState, async s => { - if (s.appLoaded) { // fs ready +const unsubscribe = watchValue(miscUiState, async s => { + if (s.fsReady && s.appConfig) { + unsubscribe() if (reconnectOptions) { sessionStorage.removeItem('reconnectOptions') if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) { @@ -905,13 +966,6 @@ document.body.addEventListener('touchstart', (e) => { }, { passive: false }) // #endregion -void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { - console.warn('Failed to load optional app config.json', error) - return {} -}).then((config: AppConfig | {}) => { - miscUiState.appConfig = config -}) - // qs open actions if (!reconnectOptions) { downloadAndOpenFile().then((downloadAction) => { diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index bdcdef3c7..6a5ab0d61 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -10,6 +10,7 @@ import { versionToNumber } from 'renderer/viewer/prepare/utils' import { getRenamedData } from 'flying-squid/dist/blockRenames' import PrismarineChatLoader from 'prismarine-chat' import { BlockModel } from 'mc-assets' +import { activeGuiAtlas } from 'renderer/viewer/lib/guiRenderer' import Generic95 from '../assets/generic_95.png' import { appReplacableResources } from './generated/resources' import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState' @@ -20,6 +21,7 @@ import { currentScaling } from './scaleInterface' import { getItemDescription } from './itemsDescriptions' import { MessageFormatPart } from './chatUtils' import { GeneralInputItem, getItemMetadata, getItemNameRaw, RenderItem } from './mineflayer/items' +import { getItemModelName } from './resourcesManager' const loadedImagesCache = new Map() const cleanLoadedImagesCache = () => { @@ -154,7 +156,10 @@ const getImageSrc = (path): string | HTMLImageElement => { return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' } -const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any }, onLoad = () => { }) => { +const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any, image = undefined as HTMLImageElement | undefined }, onLoad = () => { }) => { + if (image) { + return image + } if (!path && !texture) throw new Error('Either pass path or texture') const loadPath = (blockData ? 'blocks' : path ?? texture)! if (loadedImagesCache.has(loadPath)) { @@ -174,14 +179,19 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined return loadedImagesCache.get(loadPath) } -export const renderSlot = (slot: GeneralInputItem, debugIsQuickbar = false, fullBlockModelSupport = false): { +export type ResolvedItemModelRender = { + modelName: string, +} + +export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = false, fullBlockModelSupport = false): { texture: string, blockData?: Record & { resolvedModel: BlockModel }, scale?: number, slice?: number[], - modelName?: string + modelName?: string, + image?: HTMLImageElement } | undefined => { - let itemModelName = slot.name + let itemModelName = model.modelName const originalItemName = itemModelName const isItem = loadedData.itemsByName[itemModelName] @@ -190,24 +200,41 @@ export const renderSlot = (slot: GeneralInputItem, debugIsQuickbar = false, full // #endregion - const { customModel } = getItemMetadata(slot) - if (customModel) { - itemModelName = customModel + let itemTexture + + if (!fullBlockModelSupport) { + const atlas = activeGuiAtlas.atlas?.json + // todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works) + const item = atlas?.textures[itemModelName.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '').replace('_bottom', '')] + if (item) { + const x = item.u * atlas.width + const y = item.v * atlas.height + return { + texture: 'gui', + image: activeGuiAtlas.atlas!.image, + slice: [x, y, atlas.tileSize, atlas.tileSize], + scale: 0.25, + } + } } - let itemTexture try { assertDefined(viewer.world.itemsRenderer) - itemTexture = viewer.world.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport) ?? viewer.world.itemsRenderer.getItemTexture('item/missing_texture')! + itemTexture = + viewer.world.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport) + ?? viewer.world.itemsRenderer.getItemTexture('item/missing_texture')! } catch (err) { inGameError(`Failed to render item ${itemModelName} (original: ${originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`) itemTexture = viewer.world.itemsRenderer!.getItemTexture('block/errored')! } + + if ('type' in itemTexture) { // is item return { texture: itemTexture.type, - slice: itemTexture.slice + slice: itemTexture.slice, + modelName: itemModelName } } else { // is block @@ -227,23 +254,15 @@ const getItemName = (slot: Item | RenderItem | null) => { return text.join('') } -export const renderSlotExternal = (slot) => { - const data = renderSlot(slot) - if (!data) return - return { - imageDataUrl: data.texture === 'invsprite' ? undefined : getImage({ path: data.texture })?.src, - sprite: data.slice && data.texture !== 'invsprite' ? data.slice.map(x => x * 2) : data.slice, - displayName: getItemName(slot) ?? slot.displayName, - } -} - -const mapSlots = (slots: Array) => { +const mapSlots = (slots: Array, isJei = false) => { return slots.map((slot, i) => { // todo stateid if (!slot) return try { - const slotCustomProps = renderSlot(slot, i === bot.inventory.hotbarStart + bot.quickBarSlot) + const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot + const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }) + const slotCustomProps = renderSlot({ modelName }, debugIsQuickbar) const itemCustomName = getItemName(slot) Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName }) //@ts-expect-error @@ -310,7 +329,7 @@ const upJei = (search: string) => { return new PrismarineItem(x.id, 1) }).filter(a => a !== null) lastWindow.pwindow.win.jeiSlotsPage = 0 - lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots) + lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots, true) } export const openItemsCanvas = (type, _bot = bot as typeof bot | null) => { diff --git a/src/mineflayer/items.ts b/src/mineflayer/items.ts index 2f1b2f8d5..bb437ef4f 100644 --- a/src/mineflayer/items.ts +++ b/src/mineflayer/items.ts @@ -19,6 +19,7 @@ export type RenderItem = Pick & { components?: RenderSlotComponent[], displayName?: string + modelResolved?: boolean } type JsonString = string diff --git a/src/mineflayer/minecraft-protocol-extra.ts b/src/mineflayer/minecraft-protocol-extra.ts index ef1bfbf28..e8216a00b 100644 --- a/src/mineflayer/minecraft-protocol-extra.ts +++ b/src/mineflayer/minecraft-protocol-extra.ts @@ -13,7 +13,7 @@ export const pingServerVersion = async (ip: string, port?: number, mergeOptions: ...mergeOptions, } let latency = 0 - let fullInfo = null + let fullInfo: any = null fakeClient.autoVersionHooks = [(res) => { latency = res.latency fullInfo = res diff --git a/src/mineflayer/playerState.ts b/src/mineflayer/playerState.ts index 793f306f9..260f8e534 100644 --- a/src/mineflayer/playerState.ts +++ b/src/mineflayer/playerState.ts @@ -115,7 +115,7 @@ export class PlayerStateManager implements IPlayerState { } getEyeHeight (): number { - return bot.controlState.sneak ? 1.27 : bot.entity?.['eyeHeight'] ?? 1.62 + return bot.controlState.sneak ? 1.27 : 1.62 } isOnGround (): boolean { diff --git a/src/mineflayer/plugins/mouse.ts b/src/mineflayer/plugins/mouse.ts new file mode 100644 index 000000000..e5b5e283c --- /dev/null +++ b/src/mineflayer/plugins/mouse.ts @@ -0,0 +1,204 @@ +import { createMouse } from 'mineflayer-mouse' +import * as THREE from 'three' +import { Bot } from 'mineflayer' +import { Block } from 'prismarine-block' +import { Vec3 } from 'vec3' +import { LineMaterial } from 'three-stdlib' +import { subscribeKey } from 'valtio/utils' +import { disposeObject } from 'renderer/viewer/lib/threeJsUtils' +import { isGameActive, showModal } from '../../globalState' + +// wouldn't better to create atlas instead? +import destroyStage0 from '../../../assets/destroy_stage_0.png' +import destroyStage1 from '../../../assets/destroy_stage_1.png' +import destroyStage2 from '../../../assets/destroy_stage_2.png' +import destroyStage3 from '../../../assets/destroy_stage_3.png' +import destroyStage4 from '../../../assets/destroy_stage_4.png' +import destroyStage5 from '../../../assets/destroy_stage_5.png' +import destroyStage6 from '../../../assets/destroy_stage_6.png' +import destroyStage7 from '../../../assets/destroy_stage_7.png' +import destroyStage8 from '../../../assets/destroy_stage_8.png' +import destroyStage9 from '../../../assets/destroy_stage_9.png' +import { options } from '../../optionsStorage' +import { isCypress } from '../../standaloneUtils' +import { playerState } from '../playerState' + +function createDisplayManager (bot: Bot, scene: THREE.Scene, renderer: THREE.WebGLRenderer) { + // State + const state = { + blockBreakMesh: null as THREE.Mesh | null, + breakTextures: [] as THREE.Texture[], + } + + // Initialize break mesh and textures + const loader = new THREE.TextureLoader() + const destroyStagesImages = [ + destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4, + destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9 + ] + + for (let i = 0; i < 10; i++) { + const texture = loader.load(destroyStagesImages[i]) + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + state.breakTextures.push(texture) + } + + const breakMaterial = new THREE.MeshBasicMaterial({ + transparent: true, + blending: THREE.MultiplyBlending, + alphaTest: 0.5, + }) + state.blockBreakMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), breakMaterial) + state.blockBreakMesh.visible = false + state.blockBreakMesh.renderOrder = 999 + state.blockBreakMesh.name = 'blockBreakMesh' + scene.add(state.blockBreakMesh) + + // Update functions + function updateLineMaterial () { + const inCreative = bot.game.gameMode === 'creative' + const pixelRatio = viewer.renderer.getPixelRatio() + + viewer.world.threejsCursorLineMaterial = new LineMaterial({ + color: (() => { + switch (options.highlightBlockColor) { + case 'blue': + return 0x40_80_ff + case 'classic': + return 0x00_00_00 + default: + return inCreative ? 0x40_80_ff : 0x00_00_00 + } + })(), + linewidth: Math.max(pixelRatio * 0.7, 1) * 2, + // dashed: true, + // dashSize: 5, + }) + } + + function updateDisplay () { + if (viewer.world.threejsCursorLineMaterial) { + const { renderer } = viewer + viewer.world.threejsCursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height) + viewer.world.threejsCursorLineMaterial.dashOffset = performance.now() / 750 + } + } + beforeRenderFrame.push(updateDisplay) + + // Update cursor line material on game mode change + bot.on('game', updateLineMaterial) + // Update material when highlight color setting changes + subscribeKey(options, 'highlightBlockColor', updateLineMaterial) + + function updateBreakAnimation (block: Block | undefined, stage: number | null) { + hideBreakAnimation() + if (!state.blockBreakMesh) return // todo + if (stage === null || !block) return + + const mergedShape = bot.mouse.getMergedCursorShape(block) + if (!mergedShape) return + const { position, width, height, depth } = bot.mouse.getDataFromShape(mergedShape) + state.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001) + position.add(block.position) + state.blockBreakMesh.position.set(position.x, position.y, position.z) + state.blockBreakMesh.visible = true + + //@ts-expect-error + state.blockBreakMesh.material.map = state.breakTextures[stage] ?? state.breakTextures.at(-1) + //@ts-expect-error + state.blockBreakMesh.material.needsUpdate = true + } + + function hideBreakAnimation () { + if (state.blockBreakMesh) { + state.blockBreakMesh.visible = false + } + } + + function updateCursorBlock (data?: { block: Block }) { + if (!data?.block) { + viewer.world.setHighlightCursorBlock(null) + return + } + + const { block } = data + viewer.world.setHighlightCursorBlock(block.position, bot.mouse.getBlockCursorShapes(block).map(shape => { + return bot.mouse.getDataFromShape(shape) + })) + } + + bot.on('highlightCursorBlock', updateCursorBlock) + + bot.on('blockBreakProgressStage', updateBreakAnimation) + + bot.on('end', () => { + disposeObject(state.blockBreakMesh!, true) + scene.remove(state.blockBreakMesh!) + viewer.world.setHighlightCursorBlock(null) + }) +} + +export default (bot: Bot) => { + bot.loadPlugin(createMouse({})) + + domListeners(bot) + createDisplayManager(bot, viewer.scene, viewer.renderer) + + otherListeners() +} + +const otherListeners = () => { + bot.on('startDigging', (block) => { + customEvents.emit('digStart') + }) + + bot.on('goingToSleep', () => { + showModal({ reactType: 'bed' }) + }) + + bot.on('botArmSwingStart', (hand) => { + viewer.world.changeHandSwingingState(true, hand === 'left') + }) + + bot.on('botArmSwingEnd', (hand) => { + viewer.world.changeHandSwingingState(false, hand === 'left') + }) + + bot.on('startUsingItem', (item, slot, isOffhand, duration) => { + customEvents.emit('activateItem', item, isOffhand ? 45 : bot.quickBarSlot, isOffhand) + playerState.startUsingItem() + }) + + bot.on('stopUsingItem', () => { + playerState.stopUsingItem() + }) +} + +const domListeners = (bot: Bot) => { + document.addEventListener('mousedown', (e) => { + if (e.isTrusted && !document.pointerLockElement && !isCypress()) return + if (!isGameActive(true)) return + + if (e.button === 0) { + bot.leftClickStart() + } else if (e.button === 2) { + bot.rightClickStart() + } + }) + + document.addEventListener('mouseup', (e) => { + if (e.button === 0) { + bot.leftClickEnd() + } else if (e.button === 2) { + bot.rightClickEnd() + } + }) + + bot.mouse.beforeUpdateChecks = () => { + if (!document.hasFocus()) { + // deactive all buttons + bot.mouse.buttons.fill(false) + } + } +} diff --git a/src/mineflayer/plugins/packetsPatcher.ts b/src/mineflayer/plugins/packetsPatcher.ts new file mode 100644 index 000000000..5e93ef60f --- /dev/null +++ b/src/mineflayer/plugins/packetsPatcher.ts @@ -0,0 +1,50 @@ +export default () => { + // not plugin so its loaded earlier + customEvents.on('mineflayerBotCreated', () => { + botInit() + }) +} + +const waitingPackets = {} as Record> + +const botInit = () => { + // PATCH READING + bot._client.on('packet', (data, meta) => { + if (meta.name === 'map_chunk') { + if (data.groundUp && data.bitMap === 1 && data.chunkData.every(x => x === 0)) { + data.chunkData = Buffer.from(Array.from({ length: 12_544 }).fill(0) as any) + } + } + }) + + // PATCH WRITING + + const clientWrite = bot._client.write.bind(bot._client) + const sendAllPackets = (name: string, data: any) => { + for (const packet of waitingPackets[name]) { + clientWrite(packet.name, packet.data) + } + delete waitingPackets[name] + } + + //@ts-expect-error + bot._client.write = (name: string, data: any) => { + // if (name === 'position' || name === 'position_look' || name === 'look' || name === 'teleport_confirm') { + // const chunkX = Math.floor(bot.entity.position.x / 16) + // const chunkZ = Math.floor(bot.entity.position.z / 16) + // const loadedColumns = bot.world.getColumns() + // if (loadedColumns.some((c) => c.chunkX === chunkX && c.chunkZ === chunkZ)) { + // sendAllPackets('position', data) + // } else { + // waitingPackets['position'] = [...(waitingPackets['position'] || []), { name, data }] + // return + // } + // } + if (name === 'settings') { + data['viewDistance'] = Math.max(data['viewDistance'], 3) + } + return clientWrite(name, data) + } + + // PATCH INTERACTIONS +} diff --git a/src/mineflayer/plugins/packetsRecording.ts b/src/mineflayer/plugins/packetsRecording.ts new file mode 100644 index 000000000..f1ff18cf9 --- /dev/null +++ b/src/mineflayer/plugins/packetsRecording.ts @@ -0,0 +1,107 @@ +import { viewerConnector } from 'mcraft-fun-mineflayer' +import { PACKETS_REPLAY_FILE_EXTENSION, WORLD_STATE_FILE_EXTENSION } from 'mcraft-fun-mineflayer/build/worldState' +import { Bot } from 'mineflayer' +import CircularBuffer from 'flying-squid/dist/circularBuffer' +import { PacketsLogger } from 'mcraft-fun-mineflayer/build/packetsLogger' +import { subscribe } from 'valtio' +import { lastConnectOptions } from '../../react/AppStatusProvider' +import { packetsRecordingState } from '../../packetsReplay/packetsReplayLegacy' +import { packetsReplayState } from '../../react/state/packetsReplayState' + +const AUTO_CAPTURE_PACKETS_COUNT = 30 +let circularBuffer: CircularBuffer | undefined +let lastConnectVersion = '' + +export const localRelayServerPlugin = (bot: Bot) => { + lastConnectVersion = bot.version + let ended = false + bot.on('end', () => { + ended = true + }) + + bot.loadPlugin( + viewerConnector({ + tcpEnabled: false, + websocketEnabled: false, + }) + ) + + bot.downloadCurrentWorldState = () => { + const worldState = bot.webViewer._unstable.createStateCaptureFile() + const a = document.createElement('a') + const textContents = worldState.contents + const blob = new Blob([textContents], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + a.href = url + // add readable timestamp to filename + const timestamp = new Date().toISOString().replaceAll(/[-:Z]/g, '') + a.download = `${bot.username}-world-state-${timestamp}.${WORLD_STATE_FILE_EXTENSION}` + a.click() + URL.revokeObjectURL(url) + } + + circularBuffer = new CircularBuffer(AUTO_CAPTURE_PACKETS_COUNT) + let position = 0 + bot._client.on('writePacket' as any, (name, params) => { + circularBuffer!.add({ name, state: bot._client.state, params, isFromServer: false, timestamp: Date.now() }) + if (packetsRecordingState.active) { + packetsReplayState.packetsPlayback.push({ + name, + data: params, + isFromClient: true, + isUpcoming: false, + position: position++, + timestamp: Date.now(), + }) + } + }) + bot._client.on('packet', (data, { name }) => { + if (name === 'map_chunk') data = { x: data.x, z: data.z } + circularBuffer!.add({ name, state: bot._client.state, params: data, isFromServer: true, timestamp: Date.now() }) + if (packetsRecordingState.active) { + packetsReplayState.packetsPlayback.push({ + name, + data, + isFromClient: false, + isUpcoming: false, + position: position++, + timestamp: Date.now(), + }) + } + }) + + upPacketsReplayPanel() +} + +const upPacketsReplayPanel = () => { + if (packetsRecordingState.active && bot) { + packetsReplayState.isOpen = true + packetsReplayState.replayName = 'Recording all packets for ' + bot.username + } +} + +subscribe(packetsRecordingState, () => { + upPacketsReplayPanel() +}) + +declare module 'mineflayer' { + interface Bot { + downloadCurrentWorldState: () => void + } +} + +export const getLastAutoCapturedPackets = () => circularBuffer?.size +export const downloadAutoCapturedPackets = () => { + const logger = new PacketsLogger({ minecraftVersion: lastConnectVersion }) + for (const packet of circularBuffer?.getLastElements() ?? []) { + logger.log(packet.isFromServer, { name: packet.name, state: packet.state, time: packet.timestamp }, packet.params) + } + const textContents = logger.contents + const blob = new Blob([textContents], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${lastConnectOptions.value?.server ?? 'unknown-server'}-${lastConnectOptions.value?.username ?? 'unknown-username'}-auto-captured-packets.txt` + a.click() + URL.revokeObjectURL(url) +} diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index bbc04fa64..7032e6449 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -10,11 +10,12 @@ import Slider from './react/Slider' import { getScreenRefreshRate } from './utils' import { setLoadingScreenStatus } from './appStatus' import { openFilePicker, resetLocalStorageWithoutWorld } from './browserfs' -import { completeTexturePackInstall, getResourcePackNames, resourcePackState, uninstallResourcePack } from './resourcePack' -import { downloadPacketsReplay, packetsReplaceSessionState } from './packetsReplay' +import { completeResourcepackPackInstall, getResourcePackNames, resourcePackState, uninstallResourcePack } from './resourcePack' +import { downloadPacketsReplay, packetsRecordingState } from './packetsReplay/packetsReplayLegacy' import { showOptionsModal } from './react/SelectOption' import supportedVersions from './supportedVersions.mjs' import { getVersionAutoSelect } from './connect' +import { createNotificationProgressReporter } from './core/progressReporter' export const guiOptionsScheme: { [t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial> } & { custom? }> @@ -181,7 +182,7 @@ export const guiOptionsScheme: { } if (choice === 'Enable') { options.enabledResourcepack = name - await completeTexturePackInstall(name, name, false) + await completeResourcepackPackInstall(name, name, false, createNotificationProgressReporter()) return } if (choice === 'Uninstall') { @@ -461,18 +462,18 @@ export const guiOptionsScheme: { }, { custom () { - const { active } = useSnapshot(packetsReplaceSessionState) + const { active } = useSnapshot(packetsRecordingState) return }, }, { custom () { - const { active, hasRecordedPackets } = useSnapshot(packetsReplaceSessionState) + const { active, hasRecordedPackets } = useSnapshot(packetsRecordingState) return ) + } else if (button.type === 'url' && button.text) { + rowButtons.push() + } + } + pauseLinks.push(
{rowButtons}
) + } + } + return - -
- - -
+ {pauseLinks} {singleplayer ? (
diff --git a/src/react/ReplayPanel.stories.tsx b/src/react/ReplayPanel.stories.tsx new file mode 100644 index 000000000..70d21ea69 --- /dev/null +++ b/src/react/ReplayPanel.stories.tsx @@ -0,0 +1,106 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { proxy, useSnapshot } from 'valtio' +import { useEffect } from 'react' +import ReplayPanel, { PacketData } from './ReplayPanel' + +const meta: Meta = { + component: ReplayPanel, + title: 'ReplayPanel' +} + +export default meta +type Story = StoryObj + +const mockPackets = proxy([ + { + name: 'position', + data: { x: 100.123, y: 64.456, z: -200.789 }, + isFromClient: true, + isUpcoming: false, + position: 1, + timestamp: 1_234_567_890 + }, + { + name: 'chat', + data: { message: 'Hello, world!' }, + isFromClient: true, + isUpcoming: false, + position: 2, + timestamp: 1_234_567_890 + }, + { + name: 'block_change', + data: { blockId: 1, position: { x: 100, y: 64, z: -200 } }, + isFromClient: false, + isUpcoming: true, + position: 3, + timestamp: 1_234_567_890 + }, + { + name: 'entity_move', + data: { entityId: 1, x: 100, y: 64, z: -200 }, + isFromClient: false, + isUpcoming: false, + actualVersion: { x: 101, y: 64, z: -201 }, + position: 4, + timestamp: 1_234_567_890 + } +] satisfies PacketData[]) + +const ReplayPanelWithToggle = (props: Parameters[0]) => { + const packets = useSnapshot(mockPackets) + + useEffect(() => { + const interval = setInterval(() => { + for (const [index, packet] of mockPackets.entries()) { + packet.isUpcoming = !packet.isUpcoming + } + }, 3000) + + return () => clearInterval(interval) + }, []) + + return +} + +export const Primary: Story = { + render: () => ( + {}} + onRestart={() => {}} + onSpeedChange={() => {}} + onCustomButtonToggle={() => {}} + onFilterChange={() => {}} + packets={mockPackets} + /> + ) +} + +export const Playing: Story = { + render: () => ( + {}} + onRestart={() => {}} + onSpeedChange={() => {}} + onCustomButtonToggle={() => {}} + onFilterChange={() => {}} + packets={mockPackets} + /> + ) +} diff --git a/src/react/ReplayPanel.tsx b/src/react/ReplayPanel.tsx new file mode 100644 index 000000000..fd4082af2 --- /dev/null +++ b/src/react/ReplayPanel.tsx @@ -0,0 +1,229 @@ +import { useState, useEffect } from 'react' +import { filterPackets } from './packetsFilter' +import { DARK_COLORS } from './components/replay/constants' +import FilterInput from './components/replay/FilterInput' +import PacketList from './components/replay/PacketList' +import ProgressBar from './components/replay/ProgressBar' + +interface Props { + replayName: string + packets: readonly PacketData[] + isPlaying: boolean + progress: { current: number; total: number } + speed: number + defaultFilter?: string + customButtons: Readonly> + onPlayPause?: (isPlaying: boolean) => void + onRestart?: () => void + onSpeedChange?: (speed: number) => void + onFilterChange: (filter: string) => void + onCustomButtonToggle: (buttonId: string) => void + clientPacketsAutocomplete: string[] + serverPacketsAutocomplete: string[] + style?: React.CSSProperties +} + +export default function ReplayPanel ({ + replayName, + packets, + isPlaying, + progress, + speed, + defaultFilter = '', + customButtons, + onPlayPause, + onRestart, + onSpeedChange, + onFilterChange, + onCustomButtonToggle, + clientPacketsAutocomplete, + serverPacketsAutocomplete, + style +}: Props) { + const [filter, setFilter] = useState(defaultFilter) + const [isMinimized, setIsMinimized] = useState(false) + const { filtered: filteredPackets, hiddenCount } = filterPackets(packets.slice(-500), filter) + + useEffect(() => { + onFilterChange(filter) + }, [filter, onFilterChange]) + + const handlePlayPauseClick = () => { + if (isMinimized) { + setIsMinimized(false) + } else { + onPlayPause?.(!isPlaying) + } + } + + const playPauseButton = ( + + ) + + const baseContainerStyle = { + position: 'fixed', + top: 18, + right: 0, + zIndex: 1000, + background: DARK_COLORS.bg, + padding: '16px', + borderRadius: '0 0 8px 0', + boxShadow: '0 2px 8px rgba(0,0,0,0.3)', + display: 'flex', + flexDirection: 'column', + gap: '12px', + color: DARK_COLORS.text, + ...style + } as const + + if (isMinimized) { + return ( +
+ {playPauseButton} +
+ ) + } + + return ( +
+
+
{replayName || 'Unnamed Replay'}
+ +
+ +
Integrated server emulation. Testing client...
+ + setFilter('')} + clientPacketsAutocomplete={clientPacketsAutocomplete} + serverPacketsAutocomplete={serverPacketsAutocomplete} + /> + + + +
+ {playPauseButton} + +
+ +
+ + + onSpeedChange?.(Number(e.target.value))} + onContextMenu={e => { + e.preventDefault() + onSpeedChange?.(1) + }} + step={0.1} + min={0.1} + style={{ + width: '60px', + padding: '4px', + border: `1px solid ${DARK_COLORS.border}`, + borderRadius: '4px', + background: DARK_COLORS.input, + color: DARK_COLORS.text + }} + /> + + {Object.entries(customButtons).map(([buttonId, { state, label, tooltip }]) => ( + + ))} +
+
+ ) +} + +export interface PacketData { + name: string + data: any + isFromClient: boolean + isUpcoming: boolean + actualVersion?: any + position: number + timestamp: number +} diff --git a/src/react/ServersListProvider.tsx b/src/react/ServersListProvider.tsx index 16c5ae8b9..56ec75b60 100644 --- a/src/react/ServersListProvider.tsx +++ b/src/react/ServersListProvider.tsx @@ -17,6 +17,7 @@ import { useCopyKeybinding } from './simpleHooks' import { AuthenticatedAccount, getInitialServersList, getServerConnectionHistory, setNewServersList, StoreServerItem } from './serversStorage' type AdditionalDisplayData = { + textNameRightGrayed: string formattedText: string textNameRight: string icon?: string @@ -143,9 +144,11 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL let data if (isWebSocket) { const pingResult = await getServerInfo(server.ip, undefined, undefined, true) + console.log('pingResult.fullInfo.description', pingResult.fullInfo.description) data = { - formattedText: `${pingResult.version} server with a direct websocket connection`, + formattedText: pingResult.fullInfo.description, textNameRight: `ws ${pingResult.latency}ms`, + textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`, offline: false } } else { @@ -364,6 +367,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL detail: (server.versionOverride ?? '') + ' ' + (server.usernameOverride ?? ''), formattedTextOverride: additional?.formattedText, worldNameRight: additional?.textNameRight ?? '', + worldNameRightGrayed: additional?.textNameRightGrayed ?? '', iconSrc: additional?.icon, offline: additional?.offline } diff --git a/src/react/Singleplayer.tsx b/src/react/Singleplayer.tsx index 5effc2699..6d7d6b0e9 100644 --- a/src/react/Singleplayer.tsx +++ b/src/react/Singleplayer.tsx @@ -24,13 +24,14 @@ export interface WorldProps { detail?: string formattedTextOverride?: string worldNameRight?: string + worldNameRightGrayed?: string onFocus?: (name: string) => void onInteraction?(interaction: 'enter' | 'space') elemRef?: React.Ref offline?: boolean } -const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, elemRef, offline }: WorldProps & { ref?: React.Ref }) => { +const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, worldNameRightGrayed, elemRef, offline }: WorldProps & { ref?: React.Ref }) => { const timeRelativeFormatted = useMemo(() => { if (!lastPlayed) return '' const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }) @@ -63,6 +64,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
{title}
+ {worldNameRightGrayed && {worldNameRightGrayed}} {offline ? ( diff --git a/src/react/TouchAreasControls.tsx b/src/react/TouchAreasControls.tsx index 981ebebb5..6b34f9bcf 100644 --- a/src/react/TouchAreasControls.tsx +++ b/src/react/TouchAreasControls.tsx @@ -1,7 +1,6 @@ import { CSSProperties, PointerEvent, useEffect, useRef } from 'react' import { proxy, ref, useSnapshot } from 'valtio' import { contro } from '../controls' -import worldInteractions from '../worldInteractions' import { options } from '../optionsStorage' import PixelartIcon from './PixelartIcon' import Button from './Button' @@ -73,8 +72,9 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props) }[name] const holdDown = { action () { + if (!bot) return document.dispatchEvent(new MouseEvent('mousedown', { button: 2 })) - worldInteractions.update() + bot.mouse.update() }, sneak () { void contro.emit('trigger', { @@ -84,8 +84,9 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props) active = bot?.getControlState('sneak') }, break () { + if (!bot) return document.dispatchEvent(new MouseEvent('mousedown', { button: 0 })) - worldInteractions.update() + bot.mouse.update() active = true }, jump () { @@ -108,8 +109,9 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props) active = bot?.getControlState('sneak') }, break () { + if (!bot) return document.dispatchEvent(new MouseEvent('mouseup', { button: 0 })) - worldInteractions.update() + bot.mouse.update() active = false }, jump () { diff --git a/src/react/components/replay/FilterInput.tsx b/src/react/components/replay/FilterInput.tsx new file mode 100644 index 000000000..7f968d3e1 --- /dev/null +++ b/src/react/components/replay/FilterInput.tsx @@ -0,0 +1,162 @@ +import { useEffect, useRef, useState } from 'react' +import { DARK_COLORS } from './constants' + +interface Props { + value: string + onChange: (value: string) => void + hiddenCount: number + shownCount: number + onClearFilter: () => void + clientPacketsAutocomplete: string[] + serverPacketsAutocomplete: string[] +} + +export default function FilterInput ({ + value, + onChange, + hiddenCount, + shownCount, + onClearFilter, + clientPacketsAutocomplete, + serverPacketsAutocomplete +}: Props) { + const inputRef = useRef(null) + const [showAutocomplete, setShowAutocomplete] = useState(false) + const [selectedIndex, setSelectedIndex] = useState(0) + + const allSuggestions = [ + ...clientPacketsAutocomplete.map(name => ({ name, isClient: true })), + ...serverPacketsAutocomplete.map(name => ({ name, isClient: false })) + ].sort((a, b) => a.name.localeCompare(b.name)) + + const currentWord = value.split(/,\s*/).pop() || '' + const filteredSuggestions = allSuggestions.filter( + ({ name }) => name.toLowerCase().includes(currentWord.toLowerCase().replace(/^\$/, '')) + ) + + useEffect(() => { + setSelectedIndex(0) + }, [currentWord]) + + const acceptSuggestion = (suggestion: string) => { + const parts = value.split(/,\s*/) + parts[parts.length - 1] = suggestion + onChange(parts.join(', ')) + setShowAutocomplete(false) + inputRef.current?.focus() + } + + return ( +
+
+ onChange(e.target.value)} + onFocus={() => setShowAutocomplete(true)} + onBlur={() => { + setTimeout(() => setShowAutocomplete(false), 200) + }} + onKeyDown={e => { + if (!showAutocomplete) { + if (e.key === 'Tab') { + e.preventDefault() + setShowAutocomplete(true) + } + return + } + + if (e.key === 'ArrowDown') { + e.preventDefault() + setSelectedIndex(i => (i + 1) % filteredSuggestions.length) + } else if (e.key === 'ArrowUp') { + e.preventDefault() + setSelectedIndex(i => (i - 1 + filteredSuggestions.length) % filteredSuggestions.length) + } else if (e.key === 'Enter' && filteredSuggestions.length > 0) { + e.preventDefault() + acceptSuggestion(filteredSuggestions[selectedIndex].name) + } else if (e.key === 'Escape') { + e.preventDefault() + setShowAutocomplete(false) + } + }} + placeholder="Filter packets (e.g. entity, $block_display, !position)" + style={{ + width: '100%', + padding: '8px', + border: `1px solid ${DARK_COLORS.border}`, + borderRadius: '4px', + background: DARK_COLORS.input, + color: DARK_COLORS.text + }} + /> + {showAutocomplete && filteredSuggestions.length > 0 && ( +
+ {filteredSuggestions.map(({ name, isClient }, index) => ( +
acceptSuggestion(name)} + style={{ + padding: '4px 8px', + cursor: 'pointer', + backgroundColor: index === selectedIndex ? DARK_COLORS.hover : DARK_COLORS.bg, + color: isClient ? DARK_COLORS.client : DARK_COLORS.server + }} + > + {name} +
+ ))} +
+ )} +
+
+ Showing: {shownCount} + + Hidden: {hiddenCount} + +
+ + +
+ +
+
+ ) +} diff --git a/src/react/components/replay/PacketList.tsx b/src/react/components/replay/PacketList.tsx new file mode 100644 index 000000000..5183bf3f3 --- /dev/null +++ b/src/react/components/replay/PacketList.tsx @@ -0,0 +1,148 @@ +import { useRef, useState } from 'react' +import { processPacketDataForLogging } from 'mcraft-fun-mineflayer/build/packetsLogger' +import { PacketData } from '../../ReplayPanel' +import { useScrollBehavior } from '../../hooks/useScrollBehavior' +import { ClientOnMap } from '../../../generatedServerPackets' +import { DARK_COLORS } from './constants' + +const formatters: Record string> = { + position: (data) => `x:${data.x.toFixed(2)} y:${data.y.toFixed(2)} z:${data.z.toFixed(2)}`, + // chat: (data) => data, + map_chunk (data: ClientOnMap['map_chunk'] | any) { + const sizeOfChunk = data.chunkData?.length + const blockEntitiesCount = data.blockEntities?.length + return `x:${data.x} z:${data.z} C:${sizeOfChunk} E:${blockEntitiesCount}` + }, + default: (data) => processPacketDataForLogging(data) +} + +const getPacketIcon = (name: string): string => { + if (name.includes('position')) return '📍' + if (name.includes('chat')) return '💬' + if (name.includes('block') || name.includes('chunk') || name.includes('light')) return '📦' + if (name.includes('entity') || name.includes('player') || name.includes('passenger')) return '🎯' + return '📄' +} + +interface Props { + packets: PacketData[] + filter: string + maxHeight?: number +} + +const ROW_HEIGHT = 24 +const EXPANDED_HEIGHT = 120 + +function formatTimeDiff (current: number, prev: number | null): string { + if (prev === null) return '' + const diff = current - prev + return `+${Math.floor(diff / 1000)}` +} + +const styles = { + packetRow: { + height: ROW_HEIGHT, + padding: '0 8px', + fontSize: '12px', + display: 'flex', + alignItems: 'center', + gap: '8px', + whiteSpace: 'nowrap', + overflow: 'hidden', + cursor: 'pointer', + transition: 'background-color 0.1s' + } as const, + expandedPacket: { + height: EXPANDED_HEIGHT, + padding: '8px', + background: DARK_COLORS.input, + fontSize: '12px', + overflow: 'auto', + borderBottom: `1px solid ${DARK_COLORS.border}` + } as const +} + +export default function PacketList ({ packets, filter, maxHeight = 300 }: Props) { + const listRef = useRef(null) + const [expandedPacket, setExpandedPacket] = useState(null) + const { scrollToBottom } = useScrollBehavior(listRef, { messages: packets, opened: true }) + + let prevTimestamp: number | null = null + + return ( + <> + +
+
+ {packets.map((packet, index) => { + const timeDiff = formatTimeDiff(packet.timestamp, prevTimestamp) + prevTimestamp = packet.timestamp + return ( +
+
setExpandedPacket(expandedPacket === packet.position ? null : packet.position)} + style={{ + ...styles.packetRow, + background: packet.isFromClient ? DARK_COLORS.client : DARK_COLORS.server, + opacity: packet.isUpcoming ? 0.5 : 1 + }} + > + {getPacketIcon(packet.name)} + + #{packet.position} + {timeDiff && {timeDiff}} + + {filter && ( + #{index + 1} + )} + + {packet.name} + + + {formatters[packet.name]?.(packet.data) ?? formatters.default(packet.data)} + +
+ {expandedPacket === packet.position && ( +
+
+ Data: +
+                        {JSON.stringify(JSON.parse(formatters.default(packet.data)), null, 2)}
+                      
+
+ {packet.actualVersion && ( +
+ Actual Version: +
+                          {JSON.stringify(JSON.parse(formatters.default(packet.actualVersion)), null, 2)}
+                        
+
+ )} +
+ )} +
+ ) + })} +
+
+ + ) +} diff --git a/src/react/components/replay/ProgressBar.tsx b/src/react/components/replay/ProgressBar.tsx new file mode 100644 index 000000000..7bf5f88bd --- /dev/null +++ b/src/react/components/replay/ProgressBar.tsx @@ -0,0 +1,36 @@ +import { DARK_COLORS } from './constants' + +interface Props { + current: number + total: number +} + +function padNumber (num: number): string { + return String(num).padStart(3, '0') +} + +export default function ProgressBar ({ current, total }: Props) { + return ( +
+
+ {padNumber(current)}/{padNumber(total)} +
+
+
+
+
+ ) +} diff --git a/src/react/components/replay/constants.ts b/src/react/components/replay/constants.ts new file mode 100644 index 000000000..54beb7cbd --- /dev/null +++ b/src/react/components/replay/constants.ts @@ -0,0 +1,11 @@ +export const DARK_COLORS = { + bg: '#1e1e1e', + input: '#2d2d2d', + text: '#ffffff', + textDim: '#888888', + client: '#144218', + server: '#750200', + modified: '#f57f17', + border: '#333333', + hover: '#404040' +} diff --git a/src/react/packetsFilter.ts b/src/react/packetsFilter.ts new file mode 100644 index 000000000..e89621cf6 --- /dev/null +++ b/src/react/packetsFilter.ts @@ -0,0 +1,55 @@ +import { PacketData } from './ReplayPanel' + +function wildcardToRegExp (pattern: string): RegExp { + const escaped = pattern.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&') + return new RegExp(`^${escaped.replaceAll('\\*', '.*')}$`) +} + +function patternToRegExp (pattern: string): RegExp { + if (pattern.startsWith('$')) { + return new RegExp(`^${pattern.slice(1)}$`) + } + return wildcardToRegExp(`*${pattern}*`) +} + +export function parseFilterString (filter: string): { include: RegExp[]; exclude: RegExp[] } { + const parts = filter.split(/,\s*/) + const include: RegExp[] = [] + const exclude: RegExp[] = [] + + for (const part of parts) { + if (!part) continue + if (part.startsWith('!')) { + exclude.push(patternToRegExp(part.slice(1))) + } else { + include.push(patternToRegExp(part)) + } + } + + return { include, exclude } +} + +export function filterPackets (packets: PacketData[], filter: string): { filtered: PacketData[]; hiddenCount: number } { + if (!filter.trim()) { + return { filtered: packets, hiddenCount: 0 } + } + + const { include, exclude } = parseFilterString(filter) + const filtered = packets.filter(packet => { + // If packet matches any exclude pattern, filter it out + if (exclude.some(pattern => pattern.test(packet.name))) { + return false + } + // If there are include patterns, packet must match at least one + if (include.length > 0) { + return include.some(pattern => pattern.test(packet.name)) + } + // If no include patterns, keep the packet (unless it was excluded) + return true + }) + + return { + filtered, + hiddenCount: packets.length - filtered.length + } +} diff --git a/src/react/singleplayer.module.css b/src/react/singleplayer.module.css index 0e69ea468..0de917350 100644 --- a/src/react/singleplayer.module.css +++ b/src/react/singleplayer.module.css @@ -36,6 +36,9 @@ .world_title_right { color: #999; font-size: 9px; + display: flex; + align-items: end; + gap: 1px; } .world_info { margin-left: 3px; diff --git a/src/react/state/packetsReplayState.ts b/src/react/state/packetsReplayState.ts new file mode 100644 index 000000000..0ff475a96 --- /dev/null +++ b/src/react/state/packetsReplayState.ts @@ -0,0 +1,59 @@ +import { proxy } from 'valtio' +import type { PacketData } from '../ReplayPanel' +import { appQueryParams, updateQsParam } from '../../appParams' + +export const packetsReplayState = proxy({ + packetsPlayback: [] as PacketData[], + isOpen: false, + replayName: '', + isPlaying: false, + progress: { + current: 0, + total: 0 + }, + speed: appQueryParams.replaySpeed ? parseFloat(appQueryParams.replaySpeed) : 1, + customButtons: { + validateClientPackets: { + state: appQueryParams.replayValidateClient === 'true', + label: 'C', + tooltip: 'Validate client packets' + }, + stopOnError: { + state: appQueryParams.replayStopOnError === 'true', + label: 'E', + tooltip: 'Stop the replay when an error occurs' + }, + skipMissingOnTimeout: { + state: appQueryParams.replaySkipMissingOnTimeout === 'true', + label: 'T', + tooltip: 'Skip missing packets on timeout' + }, + packetsSenderDelay: { + state: appQueryParams.replayPacketsSenderDelay === 'true', + label: 'D', + tooltip: 'Send packets with an additional delay' + } + } +}) + +export const onChangeButtonState = (button: keyof typeof packetsReplayState.customButtons, state: boolean) => { + packetsReplayState.customButtons[button].state = state + switch (button) { + case 'validateClientPackets': { + updateQsParam('replayValidateClient', String(state)) + break + } + case 'stopOnError': { + updateQsParam('replayStopOnError', String(state)) + break + } + case 'skipMissingOnTimeout': { + updateQsParam('replaySkipMissingOnTimeout', String(state)) + break + } + case 'packetsSenderDelay': { + updateQsParam('replayPacketsSenderDelay', String(state)) + break + } + } +} diff --git a/src/reactUi.tsx b/src/reactUi.tsx index fc508a3ab..ac2dbe74b 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -51,6 +51,7 @@ import MineflayerPluginHud from './react/MineflayerPluginHud' import MineflayerPluginConsole from './react/MineflayerPluginConsole' import { UIProvider } from './react/UIProvider' import { useAppScale } from './scaleInterface' +import PacketsReplayProvider from './react/PacketsReplayProvider' const RobustPortal = ({ children, to }) => { return createPortal({children}, to) @@ -202,10 +203,11 @@ const App = () => { - + +
diff --git a/src/resourcePack.ts b/src/resourcePack.ts index d17d07b49..fd01168a1 100644 --- a/src/resourcePack.ts +++ b/src/resourcePack.ts @@ -6,7 +6,6 @@ import { proxy, subscribe } from 'valtio' import { WorldRendererThree } from 'renderer/viewer/lib/worldrendererThree' import { armorTextures } from 'renderer/viewer/lib/entity/armorModels' import { collectFilesToCopy, copyFilesAsyncWithProgress, mkdirRecursive, removeFileRecursiveAsync } from './browserfs' -import { setLoadingScreenStatus } from './appStatus' import { showNotification } from './react/NotificationProvider' import { options } from './optionsStorage' import { showOptionsModal } from './react/SelectOption' @@ -14,6 +13,7 @@ import { appStatusState } from './react/AppStatusProvider' import { appReplacableResources, resourcesContentOriginal } from './generated/resources' import { gameAdditionalState, miscUiState } from './globalState' import { watchUnloadForCleanup } from './gameUnload' +import { createConsoleLogProgressReporter, createFullScreenProgressReporter, ProgressReporter } from './core/progressReporter' export const resourcePackState = proxy({ resourcePackInstalled: false, @@ -31,13 +31,13 @@ const getLoadedImage = async (url: string) => { return img } -const texturePackBasePath = '/data/resourcePacks/' +const resourcepackPackBasePath = '/data/resourcePacks/' export const uninstallResourcePack = async (name = 'default') => { if (await existsAsync('/resourcepack/pack.mcmeta')) { await removeFileRecursiveAsync('/resourcepack') gameAdditionalState.usingServerResourcePack = false } - const basePath = texturePackBasePath + name + const basePath = resourcepackPackBasePath + name if (!(await existsAsync(basePath))) return await removeFileRecursiveAsync(basePath) options.enabledResourcepack = null @@ -47,7 +47,7 @@ export const uninstallResourcePack = async (name = 'default') => { export const getResourcePackNames = async () => { // TODO try { - return { [await fs.promises.readFile(join(texturePackBasePath, 'default', 'name.txt'), 'utf8')]: true } + return { [await fs.promises.readFile(join(resourcepackPackBasePath, 'default', 'name.txt'), 'utf8')]: true } } catch (err) { return {} } @@ -59,7 +59,7 @@ export const fromTexturePackPath = (path) => { export const updateTexturePackInstalledState = async () => { try { - resourcePackState.resourcePackInstalled = await existsAsync(texturePackBasePath + 'default') + resourcePackState.resourcePackInstalled = await existsAsync(resourcepackPackBasePath + 'default') } catch { } } @@ -70,19 +70,18 @@ export const installTexturePackFromHandle = async () => { // await completeTexturePackInstall() } -export const installResourcepackPack = async (file: File | ArrayBuffer, displayName = file['name'], name = 'default', isServer = false) => { +export const installResourcepackPack = async (file: File | ArrayBuffer, progressReporter: ProgressReporter, displayName = file['name'], name = 'default', isServer = false) => { console.time('processResourcePack') - const installPath = isServer ? '/resourcepack/' : texturePackBasePath + name + const installPath = isServer ? '/resourcepack/' : resourcepackPackBasePath + name try { - await uninstallResourcePack(name) + await progressReporter.executeWithMessage('Uninstalling resource pack', async () => { + await uninstallResourcePack(name) + }) } catch (err) { } - const showLoader = !isServer const status = 'Installing resource pack: copying all files' + progressReporter.beginStage('copy-files-resourcepack', status) - if (showLoader) { - setLoadingScreenStatus(status) - } // extract the zip and write to fs every file in it const zip = new JSZip() const zipFile = await zip.loadAsync(file) @@ -93,9 +92,7 @@ export const installResourcepackPack = async (file: File | ArrayBuffer, displayN .filter(([path]) => !path.startsWith('.') && !path.startsWith('_') && !path.startsWith('/')) // ignore dot files and __MACOSX let done = 0 const upStatus = () => { - if (showLoader) { - setLoadingScreenStatus(`${status} ${Math.round(done / allFilesArr.length * 100)}%`) - } + progressReporter.reportProgress('copy-files-resourcepack', done / allFilesArr.length) } const createdDirs = new Set() const copyTasks = [] as Array> @@ -119,27 +116,33 @@ export const installResourcepackPack = async (file: File | ArrayBuffer, displayN upStatus() })) console.timeEnd('resourcePackCopy') - await completeTexturePackInstall(displayName, name, isServer) + await completeResourcepackPackInstall(displayName, name, isServer, progressReporter) console.log('resource pack install done') console.timeEnd('processResourcePack') } // or enablement -export const completeTexturePackInstall = async (displayName: string | undefined, name: string, isServer: boolean) => { - const basePath = isServer ? '/resourcepack/' : texturePackBasePath + name +export const completeResourcepackPackInstall = async (displayName: string | undefined, name: string, isServer: boolean, progressReporter: ProgressReporter) => { + const basePath = isServer ? '/resourcepack/' : resourcepackPackBasePath + name if (displayName) { await fs.promises.writeFile(join(basePath, 'name.txt'), displayName, 'utf8') } - await updateTextures() - setLoadingScreenStatus(undefined) - showNotification('Texturepack installed & enabled') + await updateTextures(progressReporter) + if (currentErrors.length > 0) { + showNotification(`Resource pack installed & enabled with ${currentErrors.length} errors`) + console.error('Resource pack installed & enabled with errors:', currentErrors) + } else { + showNotification('Resource pack installed & enabled') + } await updateTexturePackInstalledState() if (isServer) { gameAdditionalState.usingServerResourcePack = true } else { options.enabledResourcepack = name } + + progressReporter.end() } const existsAsync = async (path) => { @@ -204,14 +207,15 @@ const getFilesMapFromDir = async (dir: string) => { return files } -export const getResourcepackTiles = async (type: 'blocks' | 'items' | 'armor', existingTextures: string[]) => { +let currentErrors = [] as string[] + +export const getResourcepackTiles = async (type: 'blocks' | 'items' | 'armor', existingTextures: string[], progressReporter: ProgressReporter) => { const basePath = await getActiveResourcepackBasePath() if (!basePath) return let firstTextureSize: number | undefined const namespaces = await fs.promises.readdir(join(basePath, 'assets')) - if (appStatusState.status) { - setLoadingScreenStatus(`Generating atlas texture for ${type}`) - } + progressReporter.beginStage(`generate-atlas-texture-${type}`, `Generating atlas texture for ${type}`) + const textures = {} as Record let path switch (type) { @@ -264,7 +268,7 @@ export const getResourcepackTiles = async (type: 'blocks' | 'items' | 'armor', e const file = basename(path) allInterestedPathsPerDir.get(dir)!.push(file) } - // filter out by readdir each dir + const allInterestedImages = [] as string[] for (const [dir, paths] of allInterestedPathsPerDir) { if (!await existsAsync(dir)) { @@ -280,34 +284,41 @@ export const getResourcepackTiles = async (type: 'blocks' | 'items' | 'armor', e const firstImageFile = allInterestedImages[0]! try { - // todo check all sizes from atlas firstTextureSize ??= await getSizeFromImage(`${firstImageFile}.png`) } catch (err) { } + // eslint-disable-next-line @typescript-eslint/no-loop-func const newTextures = Object.fromEntries(await Promise.all(allInterestedImages.map(async (image) => { - const imagePath = `${image}.png` - const contents = await fs.promises.readFile(imagePath, 'base64') - const img = await getLoadedImage(`data:image/png;base64,${contents}`) - const imageRelative = image.replace(`${texturesBasePath}/`, '').replace(`${texturesCommonBasePath}/`, '') - const textureName = isMinecraftNamespace ? imageRelative : `${namespace}:${imageRelative}` - return [textureName, img] + try { + const imagePath = `${image}.png` + const contents = await fs.promises.readFile(imagePath, 'base64') + const img = await getLoadedImage(`data:image/png;base64,${contents}`) + const imageRelative = image.replace(`${texturesBasePath}/`, '').replace(`${texturesCommonBasePath}/`, '') + const textureName = isMinecraftNamespace ? imageRelative : `${namespace}:${imageRelative}` + + return [textureName, img] + } catch (err) { + const imageRelative = image.replace(`${texturesBasePath}/`, '').replace(`${texturesCommonBasePath}/`, '') + const textureName = isMinecraftNamespace ? imageRelative : `${namespace}:${imageRelative}` + currentErrors.push(`[${imageRelative}] ${err.message}`) + return [textureName, undefined] + } }))) - Object.assign(textures, newTextures) as any + Object.assign(textures, Object.fromEntries(Object.entries(newTextures).filter(([, img]) => img !== undefined))) } return { firstTextureSize, - textures + textures, } } -const prepareBlockstatesAndModels = async () => { +const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) => { viewer.world.customBlockStates = {} viewer.world.customModels = {} - const usedTextures = new Set() + const usedBlockTextures = new Set() + const usedItemTextures = new Set() const basePath = await getActiveResourcepackBasePath() if (!basePath) return - if (appStatusState.status) { - setLoadingScreenStatus('Reading resource pack blockstates and models') - } + progressReporter.beginStage('read-resource-pack-blockstates-and-models', 'Reading resource pack blockstates and models') const readModelData = async (path: string, type: 'models' | 'blockstates', namespaceDir: string) => { if (!(await existsAsync(path))) return @@ -318,8 +329,9 @@ const prepareBlockstatesAndModels = async () => { if (file.endsWith('.json')) { const contents = await fs.promises.readFile(filePath, 'utf8') let name = file.replace('.json', '') + const isBlock = path.endsWith('block') if (type === 'models') { - name = `${path.endsWith('block') ? 'block' : 'item'}/${name}` + name = `${isBlock ? 'block' : 'item'}/${name}` } const parsed = JSON.parse(contents) if (namespaceDir === 'minecraft') { @@ -331,7 +343,11 @@ const prepareBlockstatesAndModels = async () => { if (typeof texturePath !== 'string') continue if (texturePath.startsWith('#')) continue if (!texturePath.includes(':')) texturePath = `minecraft:${texturePath}` - usedTextures.add(texturePath as string) + if (isBlock) { + usedBlockTextures.add(texturePath as string) + } else { + usedItemTextures.add(texturePath as string) + } } } } @@ -359,30 +375,60 @@ const prepareBlockstatesAndModels = async () => { viewer.world.customBlockStates = undefined viewer.world.customModels = undefined } - return { usedTextures } + return { + usedBlockTextures, + usedItemTextures + } } -const downloadAndUseResourcePack = async (url: string): Promise => { +const downloadAndUseResourcePack = async (url: string, progressReporter: ProgressReporter): Promise => { + progressReporter.beginStage('install-resource-pack', 'Installing server resource pack') try { resourcePackState.isServerInstalling = true resourcePackState.isServerDownloading = true + progressReporter.beginStage('download-resource-pack', 'Downloading server resource pack') console.log('Downloading server resource pack', url) console.time('downloadServerResourcePack') const response = await fetch(url).catch((err) => { console.log(`Ensure server on ${url} support CORS which is not required for regular client, but is required for the web client`) console.error(err) - showNotification('Failed to download resource pack: ' + err.message) + progressReporter.error('Failed to download resource pack: ' + err.message) }) console.timeEnd('downloadServerResourcePack') if (!response) return + + const contentLength = response.headers.get('Content-Length') + const total = contentLength ? parseInt(contentLength, 10) : 0 + let loaded = 0 + + const reader = response.body!.getReader() + const chunks: Uint8Array[] = [] + + // eslint-disable-next-line no-constant-condition + while (true) { + const { done, value } = await reader.read() + if (done) break + + chunks.push(value) + loaded += value.length + + if (total) { + const progress = Math.round((loaded / total) * 100) + progressReporter.reportProgress('download-resource-pack', progress / 100) + } + } + resourcePackState.isServerDownloading = false - const resourcePackData = await response.arrayBuffer() - showNotification('Installing resource pack...') - await installResourcepackPack(resourcePackData, undefined, undefined, true).catch((err) => { + const resourcePackData = await new Blob(chunks).arrayBuffer() + progressReporter.endStage('install-resource-pack') + await installResourcepackPack(resourcePackData, progressReporter, undefined, undefined, true).catch((err) => { console.error(err) showNotification('Failed to install resource pack: ' + err.message) }) + } catch (err) { + progressReporter.error('Could not install resource pack: ' + err.message) } finally { + progressReporter.endStage('download-resource-pack') resourcePackState.isServerInstalling = false resourcePackState.isServerDownloading = false } @@ -426,7 +472,7 @@ export const onAppLoad = () => { console.log('accepting resource pack') bot.acceptResourcePack() if (choice === true || choice === 'Download & Install (recommended)') { - await downloadAndUseResourcePack(packet.url).catch((err) => { + await downloadAndUseResourcePack(packet.url, createFullScreenProgressReporter()).catch((err) => { console.error(err) showNotification('Failed to download resource pack: ' + err.message) }) @@ -477,14 +523,15 @@ const updateAllReplacableTextures = async () => { const repeatArr = (arr, i) => Array.from({ length: i }, () => arr) -const updateTextures = async () => { +const updateTextures = async (progressReporter = createConsoleLogProgressReporter()) => { + currentErrors = [] const origBlocksFiles = Object.keys(viewer.world.sourceData.blocksAtlases.latest.textures) const origItemsFiles = Object.keys(viewer.world.sourceData.itemsAtlases.latest.textures) const origArmorFiles = Object.keys(armorTextures) - const { usedTextures: extraBlockTextures = new Set() } = await prepareBlockstatesAndModels() ?? {} - const blocksData = await getResourcepackTiles('blocks', [...origBlocksFiles, ...extraBlockTextures]) - const itemsData = await getResourcepackTiles('items', origItemsFiles) - const armorData = await getResourcepackTiles('armor', origArmorFiles) + const { usedBlockTextures, usedItemTextures } = await prepareBlockstatesAndModels(progressReporter) ?? {} + const blocksData = await getResourcepackTiles('blocks', [...origBlocksFiles, ...usedBlockTextures ?? []], progressReporter) + const itemsData = await getResourcepackTiles('items', [...origItemsFiles, ...usedItemTextures ?? []], progressReporter) + const armorData = await getResourcepackTiles('armor', origArmorFiles, progressReporter) await updateAllReplacableTextures() viewer.world.customTextures = {} if (blocksData) { @@ -505,6 +552,7 @@ const updateTextures = async () => { textures: armorData.textures } } + if (viewer.world.active) { await viewer.world.updateAssetsData() if (viewer.world instanceof WorldRendererThree) { @@ -532,13 +580,16 @@ export const copyServerResourcePackToRegular = async (name = 'default') => { } // Copy all files from server resource pack to regular location - const destPath = texturePackBasePath + name + const destPath = resourcepackPackBasePath + name await mkdirRecursive(destPath) - setLoadingScreenStatus('Copying server resource pack to regular location') - await copyFilesAsyncWithProgress('/resourcepack', destPath, true, ' (server -> regular)') + const reporter = createFullScreenProgressReporter() + reporter.setMessage('Copying server resource pack to user location') + await copyFilesAsyncWithProgress('/resourcepack', destPath, true, ' (server -> user)') // Complete the installation - await completeTexturePackInstall(displayName, name, false) - showNotification('Server resource pack copied to regular location') + await completeResourcepackPackInstall(displayName, name, false, reporter) + showNotification('Server resource pack copied to user location') + + reporter.end() } diff --git a/src/resourcesManager.ts b/src/resourcesManager.ts new file mode 100644 index 000000000..43c3882ed --- /dev/null +++ b/src/resourcesManager.ts @@ -0,0 +1,24 @@ +import { Item } from 'prismarine-item' +import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState' +import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' +import { playerState } from './mineflayer/playerState' +import { GeneralInputItem, getItemMetadata } from './mineflayer/items' + +export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties) => { + let itemModelName = item.name + const { customModel } = getItemMetadata(item) + if (customModel) { + itemModelName = customModel + } + + const itemSelector = playerState.getItemSelector({ + ...specificProps + }) + const modelFromDef = getItemDefinition(viewer.world.itemsDefinitionsStore, { + name: itemModelName, + version: viewer.world.texturesVersion!, + properties: itemSelector + })?.model + const model = (modelFromDef === 'minecraft:special' ? undefined : modelFromDef) ?? itemModelName + return model +} diff --git a/src/sounds/botSoundSystem.ts b/src/sounds/botSoundSystem.ts index 01cef0c50..aa33db60a 100644 --- a/src/sounds/botSoundSystem.ts +++ b/src/sounds/botSoundSystem.ts @@ -7,6 +7,7 @@ import { miscUiState } from '../globalState' import { options } from '../optionsStorage' import { loadOrPlaySound } from '../basicSounds' import { getActiveResourcepackBasePath, resourcePackState } from '../resourcePack' +import { showNotification } from '../react/NotificationProvider' import { createSoundMap, SoundMap } from './soundsMap' import { musicSystem } from './musicSystem' @@ -14,13 +15,14 @@ let soundMap: SoundMap | undefined const updateResourcePack = async () => { if (!soundMap) return - soundMap.activeResourcePackBasePath = await getActiveResourcepackBasePath() ?? undefined + // todo, rework to await + void soundMap.updateActiveResourcePackBasePath(await getActiveResourcepackBasePath() ?? undefined) } let musicInterval: ReturnType | null = null subscribeKey(miscUiState, 'gameLoaded', async () => { - if (!miscUiState.gameLoaded || !loadedData.sounds) { + if (!miscUiState.gameLoaded) { stopMusicSystem() soundMap?.quit() return @@ -28,7 +30,11 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { console.log(`Loading sounds for version ${bot.version}. Resourcepack state: ${JSON.stringify(resourcePackState)}`) soundMap = createSoundMap(bot.version) ?? undefined + globalThis.soundMap = soundMap if (!soundMap) return + if (soundMap.noVersionIdMapping) { + showNotification('No sound ID mapping for this version', undefined, true) + } void updateResourcePack() startMusicSystem() @@ -105,17 +111,16 @@ subscribeKey(miscUiState, 'gameLoaded', async () => { await playHardcodedSound(soundId, position, volume, pitch) }) - bot.on('hardcodedSoundEffectHeard', async (soundIdNum, soundCategory, position, volume, pitch) => { - const fixOffset = versionToNumber('1.20.4') === versionToNumber(bot.version) ? -1 : 0 - const soundKey = loadedData.sounds[soundIdNum + fixOffset]?.name - if (soundKey === undefined) return - await playGeneralSound(soundKey, position, volume, pitch) - }) - bot._client.on('sound_effect', async (packet) => { const soundResource = packet['soundEvent']?.resource as string | undefined - if (packet.soundId !== 0 || !soundResource) return const pos = new Vec3(packet.x / 8, packet.y / 8, packet.z / 8) + if (packet.soundId !== 0 || !soundResource) { + const soundKey = soundMap!.soundsIdToName[packet.soundId] + if (soundKey === undefined) return + await playGeneralSound(soundKey, pos, packet.volume, packet.pitch) + return + } + await playHardcodedSound(soundResource.replace('minecraft:', ''), pos, packet.volume, packet.pitch) }) diff --git a/src/sounds/soundsMap.ts b/src/sounds/soundsMap.ts index c53be8716..c461dd8da 100644 --- a/src/sounds/soundsMap.ts +++ b/src/sounds/soundsMap.ts @@ -31,35 +31,92 @@ interface SoundEntry { volume: number } +interface ResourcePackSoundEntry { + name: string + stream?: boolean + volume?: number +} + +interface ResourcePackSound { + category: string + sounds: ResourcePackSoundEntry[] +} + +interface ResourcePackSoundsJson { + [soundId: string]: ResourcePackSound +} + export class SoundMap { private readonly soundsPerName: Record + soundsIdToName: Record private readonly existingResourcePackPaths: Set - public activeResourcePackBasePath: string | undefined + private activeResourcePackBasePath: string | undefined + private activeResourcePackSoundsJson: ResourcePackSoundsJson | undefined + noVersionIdMapping = false constructor ( private readonly soundData: SoundMapData, private readonly version: string ) { - const allSoundsMajor = versionsMapToMajor(soundData.allSoundsMap) - const soundsMap = allSoundsMajor[versionToMajor(version)] ?? Object.values(allSoundsMajor)[0] - this.soundsPerName = Object.fromEntries( - Object.entries(soundsMap).map(([id, soundsStr]) => { - const sounds = soundsStr.split(',').map(s => { - const [volume, name, weight] = s.split(';') - if (isNaN(Number(volume))) throw new Error('volume is not a number') - if (isNaN(Number(weight))) { - // debugger - throw new TypeError('weight is not a number') - } - return { - file: name, - weight: Number(weight), - volume: Number(volume) - } - }) - return [id.split(';')[1], sounds] + // First try exact version match + let soundsMap = soundData.allSoundsMap[this.version] + + if (!soundsMap) { + // If no exact match, try major version + const allSoundsMajor = versionsMapToMajor(soundData.allSoundsMap) + soundsMap = allSoundsMajor[versionToMajor(version)] + + if (!soundsMap) { + // If still no match, use first available mapping + soundsMap = Object.values(allSoundsMajor)[0] + } + + this.noVersionIdMapping = true + } + + // Create both mappings + this.soundsIdToName = {} + this.soundsPerName = {} + + for (const [id, soundsStr] of Object.entries(soundsMap)) { + const sounds = soundsStr.split(',').map(s => { + const [volume, name, weight] = s.split(';') + if (isNaN(Number(volume))) throw new Error('volume is not a number') + if (isNaN(Number(weight))) throw new TypeError('weight is not a number') + return { + file: name, + weight: Number(weight), + volume: Number(volume) + } }) - ) + + const [idPart, namePart] = id.split(';') + this.soundsIdToName[idPart] = namePart + + this.soundsPerName[namePart] = sounds + } + } + + async updateActiveResourcePackBasePath (basePath: string | undefined) { + this.activeResourcePackBasePath = basePath + if (!basePath) { + this.activeResourcePackSoundsJson = undefined + return + } + + let soundsJsonContent: string | undefined + try { + const soundsJsonPath = path.join(basePath, 'assets/minecraft/sounds.json') + soundsJsonContent = await fs.promises.readFile(soundsJsonPath, 'utf8') + } catch (err) {} + try { + if (soundsJsonContent) { + this.activeResourcePackSoundsJson = JSON.parse(soundsJsonContent) + } + } catch (err) { + console.warn('Failed to parse sounds.json from resourcepack', err) + this.activeResourcePackSoundsJson = undefined + } } async updateExistingResourcePackPaths () { @@ -84,6 +141,38 @@ export class SoundMap { } async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number } | undefined> { + // First check resource pack sounds.json + if (this.activeResourcePackSoundsJson && soundKey in this.activeResourcePackSoundsJson) { + const rpSound = this.activeResourcePackSoundsJson[soundKey] + // Pick a random sound from the resource pack + const sound = rpSound.sounds[Math.floor(Math.random() * rpSound.sounds.length)] + const soundVolume = sound.volume ?? 1 + + if (this.activeResourcePackBasePath) { + const tryFormat = async (format: string) => { + try { + const resourcePackPath = path.join(this.activeResourcePackBasePath!, `/assets/minecraft/sounds/${sound.name}.${format}`) + const fileData = await fs.promises.readFile(resourcePackPath) + return { + url: `data:audio/${format};base64,${fileData.toString('base64')}`, + volume: soundVolume * Math.max(Math.min(volume, 1), 0) + } + } catch (err) { + return null + } + } + + const result = await tryFormat(this.soundData.soundsMeta.format) + if (result) return result + + if (this.soundData.soundsMeta.format !== 'ogg') { + const oggResult = await tryFormat('ogg') + if (oggResult) return oggResult + } + } + } + + // Fall back to vanilla sounds if no resource pack sound found const sounds = this.soundsPerName[soundKey] if (!sounds?.length) return undefined @@ -97,30 +186,13 @@ export class SoundMap { const versionedSound = this.getVersionedSound(sound.file) - let url = this.soundData.soundsMeta.baseUrl.replace(/\/$/, '') + + const url = this.soundData.soundsMeta.baseUrl.replace(/\/$/, '') + (versionedSound ? `/${versionedSound}` : '') + '/minecraft/sounds/' + sound.file + '.' + this.soundData.soundsMeta.format - // Try loading from resource pack file first - if (this.activeResourcePackBasePath) { - const tryFormat = async (format: string) => { - try { - const resourcePackPath = path.join(this.activeResourcePackBasePath!, `/assets/minecraft/sounds/${sound.file}.${format}`) - const fileData = await fs.promises.readFile(resourcePackPath) - url = `data:audio/${format};base64,${fileData.toString('base64')}` - return true - } catch (err) { - } - } - const success = await tryFormat(this.soundData.soundsMeta.format) - if (!success && this.soundData.soundsMeta.format !== 'ogg') { - await tryFormat('ogg') - } - } - return { url, volume: sound.volume * Math.max(Math.min(volume, 1), 0) @@ -188,7 +260,6 @@ const blockSoundAliases: BlockSoundMap = { bamboo: 'grass', vine: 'grass', nether_sprouts: 'grass', - nether_wart: 'grass', twisting_vines: 'grass', weeping_vines: 'grass', sweet_berry_bush: 'grass', @@ -204,6 +275,28 @@ const blockSoundAliases: BlockSoundMap = { azalea_leaves: 'grass', flowering_azalea_leaves: 'grass', + // Dirt and ground blocks + dirt: 'gravel', + coarse_dirt: 'gravel', + podzol: 'gravel', + mycelium: 'gravel', + farmland: 'gravel', + dirt_path: 'gravel', + rooted_dirt: 'rooted_dirt', + + // Crop blocks + wheat: 'crop', + potatoes: 'crop', + carrots: 'crop', + beetroots: 'crop', + melon_stem: 'crop', + pumpkin_stem: 'crop', + sweet_berries: 'crop', + cocoa: 'crop', + nether_wart: 'crop', + torchflower_crop: 'crop', + pitcher_crop: 'crop', + // Stone-like blocks cobblestone: 'stone', stone_bricks: 'stone', @@ -341,7 +434,6 @@ const blockSoundAliases: BlockSoundMap = { soul_lantern: 'lantern', pointed_dripstone: 'pointed_dripstone', dripstone_block: 'dripstone_block', - rooted_dirt: 'rooted_dirt', sculk_sensor: 'sculk_sensor', shroomlight: 'shroomlight' } diff --git a/src/supportedVersions.mjs b/src/supportedVersions.mjs index 39fd38d12..87dd5ea17 100644 --- a/src/supportedVersions.mjs +++ b/src/supportedVersions.mjs @@ -5,4 +5,6 @@ export const ignoredVersionsRegex = /(^0\.30c$)|w|-pre|-rc/ /** @type {string[]} */ const versionsFromProtocol = Object.values(postNettyVersionsByProtocolVersion.pc).flat().filter(x => !ignoredVersionsRegex.test(x.minecraftVersion)).map(x => x.minecraftVersion) +export const notTestedVersions = '1.19.3 1.20 1.19.1 1.19 1.18.1 1.15.1 1.14.1'.split(' ') + export default versionsFromProtocol.filter(x => x !== '1.7' && !x.startsWith('1.7.')) diff --git a/src/utils.ts b/src/utils.ts index a5867a418..8039d4038 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ -import { isGameActive, miscUiState } from './globalState' +import { gameAdditionalState, isGameActive, miscUiState } from './globalState' import { options } from './optionsStorage' import { notificationProxy, showNotification } from './react/NotificationProvider' +import { packetsReplayState } from './react/state/packetsReplayState' export const goFullscreen = async (doToggle = false) => { if (!document.fullscreenElement) { @@ -64,6 +65,10 @@ export const pointerLock = { } } +export const isInRealGameSession = () => { + return isGameActive(true) && !packetsReplayState.isOpen && !gameAdditionalState.viewerConnection +} + window.getScreenRefreshRate = getScreenRefreshRate /** diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 724fb2ba4..3e607f49a 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -75,7 +75,7 @@ export const watchOptionsAfterViewerInit = () => { viewer.world.mesherConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting; (viewer.world as WorldRendererThree).rerenderAllChunks() }) - customEvents.on('gameLoaded', () => { + customEvents.on('mineflayerBotCreated', () => { viewer.world.mesherConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting }) diff --git a/src/worldInteractions.ts b/src/worldInteractions.ts deleted file mode 100644 index e54b0ebeb..000000000 --- a/src/worldInteractions.ts +++ /dev/null @@ -1,550 +0,0 @@ -//@ts-check - -import * as THREE from 'three' - -// wouldn't better to create atlas instead? -import { Vec3 } from 'vec3' -import { LineMaterial } from 'three-stdlib' -import { Entity } from 'prismarine-entity' -import { Block } from 'prismarine-block' -import { subscribeKey } from 'valtio/utils' -import destroyStage0 from '../assets/destroy_stage_0.png' -import destroyStage1 from '../assets/destroy_stage_1.png' -import destroyStage2 from '../assets/destroy_stage_2.png' -import destroyStage3 from '../assets/destroy_stage_3.png' -import destroyStage4 from '../assets/destroy_stage_4.png' -import destroyStage5 from '../assets/destroy_stage_5.png' -import destroyStage6 from '../assets/destroy_stage_6.png' -import destroyStage7 from '../assets/destroy_stage_7.png' -import destroyStage8 from '../assets/destroy_stage_8.png' -import destroyStage9 from '../assets/destroy_stage_9.png' - -import { hideCurrentModal, isGameActive, showModal } from './globalState' -import { assertDefined } from './utils' -import { options } from './optionsStorage' -import { itemBeingUsed } from './react/Crosshair' -import { isCypress } from './standaloneUtils' -import { displayClientChat } from './botUtils' -import { playerState } from './mineflayer/playerState' - -function getViewDirection (pitch, yaw) { - const csPitch = Math.cos(pitch) - const snPitch = Math.sin(pitch) - const csYaw = Math.cos(yaw) - const snYaw = Math.sin(yaw) - return new Vec3(-snYaw * csPitch, snPitch, -csYaw * csPitch) -} - -class WorldInteraction { - ready = false - cursorBlock: Block | null = null - prevBreakState: number | null = null - currentDigTime: number | null = null - prevOnGround: boolean | null = null - lastBlockPlaced: number - lastSwing = 0 - buttons = [false, false, false] - lastButtons = [false, false, false] - breakStartTime: number | undefined = 0 - lastDugBlock: Vec3 | null = null - blockBreakMesh: THREE.Mesh - breakTextures: THREE.Texture[] - lastDigged: number - debugDigStatus: string - currentBreakBlock: { block: any, stage: number } | null = null - swingTimeout: any = null - - oneTimeInit () { - const loader = new THREE.TextureLoader() - this.breakTextures = [] - const destroyStagesImages = [ - destroyStage0, - destroyStage1, - destroyStage2, - destroyStage3, - destroyStage4, - destroyStage5, - destroyStage6, - destroyStage7, - destroyStage8, - destroyStage9 - ] - for (let i = 0; i < 10; i++) { - const texture = loader.load(destroyStagesImages[i]) - texture.magFilter = THREE.NearestFilter - texture.minFilter = THREE.NearestFilter - this.breakTextures.push(texture) - } - const breakMaterial = new THREE.MeshBasicMaterial({ - transparent: true, - blending: THREE.MultiplyBlending, - alphaTest: 0.5, - }) - this.blockBreakMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), breakMaterial) - this.blockBreakMesh.visible = false - this.blockBreakMesh.renderOrder = 999 - this.blockBreakMesh.name = 'blockBreakMesh' - viewer.scene.add(this.blockBreakMesh) - - // Setup events - document.addEventListener('mouseup', (e) => { - this.buttons[e.button] = false - }) - - this.lastBlockPlaced = 4 // ticks since last placed - document.addEventListener('mousedown', (e) => { - if (e.isTrusted && !document.pointerLockElement && !isCypress()) return - if (!isGameActive(true)) return - this.buttons[e.button] = true - - const entity = getEntityCursor() - - if (entity) { - if (e.button === 0) { // left click - bot.attack(entity) - } else if (e.button === 2) { // right click - this.activateEntity(entity) - } - } - }) - document.addEventListener('blur', (e) => { - this.buttons = [false, false, false] - }) - - beforeRenderFrame.push(() => { - if (viewer.world.threejsCursorLineMaterial) { - const { renderer } = viewer - viewer.world.threejsCursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height) - viewer.world.threejsCursorLineMaterial.dashOffset = performance.now() / 750 - } - }) - } - - initBot () { - if (!this.ready) { - this.ready = true - this.oneTimeInit() - } - assertDefined(viewer) - bot.on('physicsTick', () => { if (this.lastBlockPlaced < 4) this.lastBlockPlaced++ }) - bot.on('diggingCompleted', (block) => { - this.breakStartTime = undefined - this.lastDugBlock = block.position - // TODO: If the tool and enchantments immediately exceed the hardness times 30, the block breaks with no delay; SO WE NEED TO CHECK THAT - // TODO: Any blocks with a breaking time of 0.05 - this.lastDigged = Date.now() - this.debugDigStatus = 'done' - this.stopBreakAnimation() - }) - bot.on('diggingAborted', (block) => { - if (!viewer.world.cursorBlock?.equals(block.position)) return - this.debugDigStatus = 'aborted' - this.breakStartTime = undefined - if (this.buttons[0]) { - this.buttons[0] = false - this.update() - this.buttons[0] = true // trigger again - } - this.lastDugBlock = null - this.stopBreakAnimation() - }) - bot.on('heldItemChanged' as any, () => { - itemBeingUsed.name = null - }) - - // Add new event listeners for block breaking and swinging - bot.on('entitySwingArm', (entity: Entity) => { - if (entity.id === bot.entity.id) { - if (this.swingTimeout) { - clearTimeout(this.swingTimeout) - } - viewer.world.changeHandSwingingState(true, false) - this.swingTimeout = setTimeout(() => { - viewer.world.changeHandSwingingState(false, false) - this.swingTimeout = null - }, 250) - } - }) - - //@ts-expect-error mineflayer types are wrong - bot.on('blockBreakProgressObserved', (block: Block, destroyStage: number, entity: Entity) => { - if (this.cursorBlock?.position.equals(block.position) && entity.id === bot.entity.id) { - if (!this.buttons[0]) { - // Simulate left mouse button press - this.buttons[0] = true - this.update() - } - // this.setBreakState(block, destroyStage) - } - }) - - //@ts-expect-error mineflayer types are wrong - bot.on('blockBreakProgressEnd', (block: Block, entity: Entity) => { - if (this.currentBreakBlock?.block.position.equals(block.position) && entity.id === bot.entity.id) { - if (!this.buttons[0]) { - // Simulate left mouse button press - this.buttons[0] = false - this.update() - } - // this.stopBreakAnimation() - } - }) - - // Handle acknowledge_player_digging packet - bot._client.on('acknowledge_player_digging', (data: { location: { x: number, y: number, z: number }, block: number, status: number, successful: boolean } | { sequenceId: number }) => { - if ('location' in data && !data.successful) { - const packetPos = new Vec3(data.location.x, data.location.y, data.location.z) - if (this.cursorBlock?.position.equals(packetPos)) { - this.buttons[0] = false - this.update() - this.stopBreakAnimation() - } - } - }) - - const upLineMaterial = () => { - const inCreative = bot.game.gameMode === 'creative' - const pixelRatio = viewer.renderer.getPixelRatio() - viewer.world.threejsCursorLineMaterial = new LineMaterial({ - color: (() => { - switch (options.highlightBlockColor) { - case 'blue': - return 0x40_80_ff - case 'classic': - return 0x00_00_00 - default: - return inCreative ? 0x40_80_ff : 0x00_00_00 - } - })(), - linewidth: Math.max(pixelRatio * 0.7, 1) * 2, - // dashed: true, - // dashSize: 5, - }) - } - upLineMaterial() - // todo use gamemode update only - bot.on('game', upLineMaterial) - // Update material when highlight color setting changes - subscribeKey(options, 'highlightBlockColor', upLineMaterial) - } - - activateEntity (entity: Entity) { - // mineflayer has completely wrong implementation of this action - if (bot.supportFeature('armAnimationBeforeUse')) { - bot.swingArm('right') - } - bot._client.write('use_entity', { - target: entity.id, - mouse: 2, - // todo do not fake - x: 0.581_012_585_759_162_9, - y: 0.581_012_585_759_162_9, - z: 0.581_012_585_759_162_9, - // x: raycastPosition.x - entity.position.x, - // y: raycastPosition.y - entity.position.y, - // z: raycastPosition.z - entity.position.z - sneaking: bot.getControlState('sneak'), - hand: 0 - }) - bot._client.write('use_entity', { - target: entity.id, - mouse: 0, - sneaking: bot.getControlState('sneak'), - hand: 0 - }) - if (!bot.supportFeature('armAnimationBeforeUse')) { - bot.swingArm('right') - } - } - - beforeUpdateChecks () { - if (!document.hasFocus()) { - // deactive all buttson - this.buttons.fill(false) - } - } - - // todo this shouldnt be done in the render loop, migrate the code to dom events to avoid delays on lags - update () { - this.beforeUpdateChecks() - const inSpectator = bot.game.gameMode === 'spectator' - const inAdventure = bot.game.gameMode === 'adventure' - const entity = getEntityCursor() - let _cursorBlock = inSpectator && !options.showCursorBlockInSpectator ? null : bot.blockAtCursor(5) - if (entity) { - _cursorBlock = null - } - this.cursorBlock = _cursorBlock - const { cursorBlock } = this - - let cursorBlockDiggable = cursorBlock - if (cursorBlock && (!bot.canDigBlock(cursorBlock) || inAdventure) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null - - const cursorChanged = cursorBlock && viewer.world.cursorBlock ? !viewer.world.cursorBlock.equals(cursorBlock.position) : viewer.world.cursorBlock !== cursorBlock - - // Place / interact / activate - if (this.buttons[2] && this.lastBlockPlaced >= 4) { - const activatableItems = (itemName: string) => { - return ['egg', 'fishing_rod', 'firework_rocket', - 'fire_charge', 'snowball', 'ender_pearl', 'experience_bottle', 'potion', - 'glass_bottle', 'bucket', 'water_bucket', 'lava_bucket', 'milk_bucket', - 'minecart', 'boat', 'tnt_minecart', 'chest_minecart', 'hopper_minecart', - 'command_block_minecart', 'armor_stand', 'lead', 'name_tag', - // - 'writable_book', 'written_book', 'compass', 'clock', 'filled_map', 'empty_map', 'map', - 'shears', 'carrot_on_a_stick', 'warped_fungus_on_a_stick', - 'spawn_egg', 'trident', 'crossbow', 'elytra', 'shield', 'turtle_helmet', 'bow', 'crossbow', 'bucket_of_cod', - ...loadedData.foodsArray.map((f) => f.name), - ].includes(itemName) - } - const activate = bot.heldItem && activatableItems(bot.heldItem.name) - let stop = false - if (!bot.controlState.sneak) { - if (cursorBlock?.name === 'bed' || cursorBlock?.name.endsWith('_bed')) { - stop = true - showModal({ reactType: 'bed' }) - let cancelSleep = true - void bot.sleep(cursorBlock).catch((e) => { - if (cancelSleep) { - hideCurrentModal() - } - // if (e.message === 'bot is not sleeping') return - displayClientChat(e.message) - }) - setTimeout(() => { - cancelSleep = false - }) - } - } - // todo placing with offhand - if (cursorBlock && !activate && !stop) { - const vecArray = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)] - //@ts-expect-error - const delta = cursorBlock.intersect.minus(cursorBlock.position) - - if (bot.heldItem) { - //@ts-expect-error todo - bot._placeBlockWithOptions(cursorBlock, vecArray[cursorBlock.face], { delta, forceLook: 'ignore' }).catch(console.warn) - } else { - // https://discord.com/channels/413438066984747026/413438150594265099/1198724637572477098 - const oldLookAt = bot.lookAt - //@ts-expect-error - bot.lookAt = (pos) => { } - //@ts-expect-error - // TODO it still must 1. fire block place 2. swing arm (right) - bot.activateBlock(cursorBlock, vecArray[cursorBlock.face], delta).finally(() => { - bot.lookAt = oldLookAt - }).catch(console.warn) - } - viewer.world.changeHandSwingingState(true, false) - viewer.world.changeHandSwingingState(false, false) - } else if (!stop) { - const offhand = activate ? false : activatableItems(bot.inventory.slots[45]?.name ?? '') - bot.activateItem(offhand) // todo offhand - const item = offhand ? bot.inventory.slots[45] : bot.heldItem - if (item) { - customEvents.emit('activateItem', item, offhand ? 45 : bot.quickBarSlot, offhand) - } - playerState.startUsingItem() - itemBeingUsed.name = (offhand ? bot.inventory.slots[45]?.name : bot.heldItem?.name) ?? null - itemBeingUsed.hand = offhand ? 1 : 0 - } - this.lastBlockPlaced = 0 - } - // stop using activated item (cancel) - if (itemBeingUsed.name && !this.buttons[2]) { - itemBeingUsed.name = null - // "only foods and bow can be deactivated" - not true, shields also can be deactivated and client always sends this - // if (bot.heldItem && (loadedData.foodsArray.map((f) => f.name).includes(bot.heldItem.name) || bot.heldItem.name === 'bow')) { - bot.deactivateItem() - playerState.stopUsingItem() - // } - } - - // Stop break - if ((!this.buttons[0] && this.lastButtons[0]) || cursorChanged) { - try { - bot.stopDigging() // this shouldnt throw anything... - } catch (e) { } // to be reworked in mineflayer, then remove the try here - } - // We stopped breaking - if ((!this.buttons[0] && this.lastButtons[0])) { - this.lastDugBlock = null - this.breakStartTime = undefined - this.debugDigStatus = 'cancelled' - this.stopBreakAnimation() - } - - const onGround = bot.entity.onGround || bot.game.gameMode === 'creative' - this.prevOnGround ??= onGround // todo this should be fixed in mineflayer to involve correct calculations when this changes as this is very important when mining straight down // todo this should be fixed in mineflayer to involve correct calculations when this changes as this is very important when mining straight down // todo this should be fixed in mineflayer to involve correct calculations when this changes as this is very important when mining straight down - // Start break - // todo last check doesnt work as cursorChanged happens once (after that check is false) - if ( - this.buttons[0] - ) { - if (cursorBlockDiggable - && (!this.lastButtons[0] || ((cursorChanged || (this.lastDugBlock && !this.lastDugBlock.equals(cursorBlock!.position))) && Date.now() - (this.lastDigged ?? 0) > 300) || onGround !== this.prevOnGround) - && onGround) { - this.lastDugBlock = null - this.debugDigStatus = 'breaking' - this.currentDigTime = bot.digTime(cursorBlockDiggable) - this.breakStartTime = performance.now() - const vecArray = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)] - bot.dig( - //@ts-expect-error - cursorBlockDiggable, 'ignore', vecArray[cursorBlockDiggable.face] - ).catch((err) => { - if (err.message === 'Digging aborted') return - throw err - }) - customEvents.emit('digStart') - this.lastDigged = Date.now() - viewer.world.changeHandSwingingState(true, false) - } else if (performance.now() - this.lastSwing > 200) { - bot.swingArm('right') - this.lastSwing = performance.now() - } - } - if (!this.buttons[0] && this.lastButtons[0]) { - viewer.world.changeHandSwingingState(false, false) - } - this.prevOnGround = onGround - - // Show cursor - const allShapes = [...cursorBlock?.shapes ?? [], ...cursorBlock?.['interactionShapes'] ?? []] - if (cursorBlock) { - // BREAK MESH - // union of all values - const breakShape = allShapes.reduce((acc, cur) => { - return [ - Math.min(acc[0], cur[0]), - Math.min(acc[1], cur[1]), - Math.min(acc[2], cur[2]), - Math.max(acc[3], cur[3]), - Math.max(acc[4], cur[4]), - Math.max(acc[5], cur[5]) - ] - }) - const { position, width, height, depth } = getDataFromShape(breakShape) - this.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001) - position.add(cursorBlock.position) - this.blockBreakMesh.position.set(position.x, position.y, position.z) - } - - // Show break animation - if (cursorBlockDiggable && this.breakStartTime && bot.game.gameMode !== 'creative') { - const elapsed = performance.now() - this.breakStartTime - const time = bot.digTime(cursorBlockDiggable) - if (time !== this.currentDigTime) { - console.warn('dig time changed! cancelling!', time, 'from', this.currentDigTime) // todo - try { bot.stopDigging() } catch { } - } - const state = Math.floor((elapsed / time) * 10) - if (state !== this.prevBreakState) { - this.setBreakState(cursorBlockDiggable, Math.min(state, 9)) - } - this.prevBreakState = state - } else { - this.blockBreakMesh.visible = false - } - - // Update state - if (cursorChanged) { - viewer.world.setHighlightCursorBlock(cursorBlock?.position ?? null, allShapes.map(shape => { - return getDataFromShape(shape) - })) - } - this.lastButtons[0] = this.buttons[0] - this.lastButtons[1] = this.buttons[1] - this.lastButtons[2] = this.buttons[2] - } - - setBreakState (block: Block, stage: number) { - this.currentBreakBlock = { block, stage } - this.blockBreakMesh.visible = true - //@ts-expect-error - this.blockBreakMesh.material.map = this.breakTextures[stage] ?? this.breakTextures.at(-1) - //@ts-expect-error - this.blockBreakMesh.material.needsUpdate = true - } - - stopBreakAnimation () { - this.currentBreakBlock = null - this.blockBreakMesh.visible = false - } -} - -const getDataFromShape = (shape) => { - const width = shape[3] - shape[0] - const height = shape[4] - shape[1] - const depth = shape[5] - shape[2] - const centerX = (shape[3] + shape[0]) / 2 - const centerY = (shape[4] + shape[1]) / 2 - const centerZ = (shape[5] + shape[2]) / 2 - const position = new Vec3(centerX, centerY, centerZ) - return { position, width, height, depth } -} - -// Blocks that can be interacted with in adventure mode -const activatableBlockPatterns = [ - // Containers - /^(chest|barrel|hopper|dispenser|dropper)$/, - /^.*shulker_box$/, - /^.*(furnace|smoker)$/, - /^(brewing_stand|beacon)$/, - // Crafting - /^.*table$/, - /^(grindstone|stonecutter|loom)$/, - /^.*anvil$/, - // Redstone - /^(lever|repeater|comparator|daylight_detector|observer|note_block|jukebox|bell)$/, - // Buttons - /^.*button$/, - // Doors and trapdoors - /^.*door$/, - /^.*trapdoor$/, - // Functional blocks - /^(enchanting_table|lectern|composter|respawn_anchor|lodestone|conduit)$/, - /^.*bee.*$/, - // Beds - /^.*bed$/, - // Misc - /^(cake|decorated_pot|crafter|trial_spawner|vault)$/ -] - -function isBlockActivatable (blockName: string) { - return activatableBlockPatterns.some(pattern => pattern.test(blockName)) -} - -function isLookingAtActivatableBlock (block: Block) { - return isBlockActivatable(block.name) -} - -export const getEntityCursor = () => { - const entity = bot.nearestEntity((e) => { - if (e.position.distanceTo(bot.entity.position) <= (bot.game.gameMode === 'creative' ? 5 : 3)) { - const dir = getViewDirection(bot.entity.pitch, bot.entity.yaw) - const { width, height } = e - const { x: eX, y: eY, z: eZ } = e.position - const { x: bX, y: bY, z: bZ } = bot.entity.position - const box = new THREE.Box3( - new THREE.Vector3(eX - width / 2, eY, eZ - width / 2), - new THREE.Vector3(eX + width / 2, eY + height, eZ + width / 2) - ) - - const r = new THREE.Raycaster( - new THREE.Vector3(bX, bY + 1.52, bZ), - new THREE.Vector3(dir.x, dir.y, dir.z) - ) - const int = r.ray.intersectBox(box, new THREE.Vector3(eX, eY, eZ)) - return int !== null - } - - return false - }) - return entity -} - -const worldInteraction = new WorldInteraction() -globalThis.worldInteraction = worldInteraction -export default worldInteraction