rel (#232)
This commit is contained in:
commit
8dd6715b3e
60 changed files with 7314 additions and 478 deletions
|
|
@ -6,3 +6,4 @@ generated
|
|||
dist
|
||||
public
|
||||
**/*/rsbuildSharedConfig.ts
|
||||
src/mcDataTypes.ts
|
||||
11
.github/workflows/next-deploy.yml
vendored
11
.github/workflows/next-deploy.yml
vendored
|
|
@ -38,9 +38,8 @@ jobs:
|
|||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
||||
id: deploy
|
||||
- name: Set deployment alias
|
||||
run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ secrets.TEST_PREVIEW_DOMAIN }} --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
# - uses: mshick/add-pr-comment@v2
|
||||
# with:
|
||||
# message: |
|
||||
# Deployed to Vercel Preview: ${{ steps.deploy.outputs.stdout }}
|
||||
- name: Set deployment aliases
|
||||
run: |
|
||||
for alias in $(echo ${{ secrets.TEST_PREVIEW_DOMAIN }} | tr "," "\n"); do
|
||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
done
|
||||
|
|
|
|||
16
.github/workflows/preview.yml
vendored
16
.github/workflows/preview.yml
vendored
|
|
@ -55,6 +55,9 @@ jobs:
|
|||
run: npm install --global vercel
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Write Release Info
|
||||
run: |
|
||||
echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\"}" > assets/release.json
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- run: pnpm build-storybook
|
||||
|
|
@ -63,6 +66,14 @@ jobs:
|
|||
mkdir -p .vercel/output/static/playground
|
||||
pnpm build-playground
|
||||
cp -r prismarine-viewer/dist/* .vercel/output/static/playground/
|
||||
- name: Write pr redirect index.html
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/pr
|
||||
echo "<meta http-equiv='refresh' content='0;url=https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number || github.event.pull_request.number }}'>" > .vercel/output/static/pr/index.html
|
||||
- name: Write commit redirect index.html
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/commit
|
||||
echo "<meta http-equiv='refresh' content='0;url=https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number || github.event.pull_request.number }}/commits/${{ github.event.pull_request.head.sha }}'>" > .vercel/output/static/commit/index.html
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
|
|
@ -81,4 +92,7 @@ jobs:
|
|||
# - run: git checkout next scripts/githubActions.mjs
|
||||
- name: Set deployment alias
|
||||
if: ${{ steps.alias.outputs.alias != '' && steps.alias.outputs.alias != 'mcraft.fun' && steps.alias.outputs.alias != 's.mcraft.fun' }}
|
||||
run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ steps.alias.outputs.alias }} --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
run: |
|
||||
for alias in $(echo ${{ steps.alias.outputs.alias }} | tr "," "\n"); do
|
||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
done
|
||||
|
|
|
|||
|
|
@ -128,6 +128,10 @@ Press `Y` to set query parameters to url of your current game state.
|
|||
|
||||
There are some parameters you can set in the url to archive some specific behaviors:
|
||||
|
||||
General:
|
||||
|
||||
- **`?setting=<setting_name>:<setting_value>`** - Set and lock the setting on load. You can set multiple settings by separating them with `&` e.g. `?setting=autoParkour:true&setting=renderDistance:4`
|
||||
|
||||
Server specific:
|
||||
|
||||
- `?ip=<server_address>` - Display connect screen to the server on load with predefined server ip. `:<port>` is optional and can be added to the ip.
|
||||
|
|
@ -137,6 +141,7 @@ Server specific:
|
|||
- `?username=<username>` - Set the username for the server
|
||||
- `?lockConnect=true` - Only works then `ip` parameter is set. Disables cancel/save buttons and all inputs in the connect screen already set as parameters. Useful for integrates iframes.
|
||||
- `?reconnect=true` - Reconnect to the server on page reloads. Available in **dev mode only** and very useful on server testing.
|
||||
- `?serversList=<list_or_url>` - `<list_or_url>` can be a list of servers in the format `ip:version,ip` or a url to a json file with the same format (array) or a txt file with line-delimited list of server IPs.
|
||||
|
||||
Single player specific:
|
||||
|
||||
|
|
@ -171,10 +176,6 @@ In this case you must use `?mapDirBaseUrl` to specify the base URL to fetch the
|
|||
|
||||
<!-- - `?mapDirGuess=<base_url>` - Load the map from the provided URL and paths will be guessed with a few additional fetch requests. -->
|
||||
|
||||
General:
|
||||
|
||||
- `?setting=<setting_name>:<setting_value>` - Set and lock the setting on load. You can set multiple settings by separating them with `&` e.g. `?setting=autoParkour:true&setting=renderDistance:4`
|
||||
|
||||
### Notable Things that Power this Project
|
||||
|
||||
- [Mineflayer](https://github.com/PrismarineJS/mineflayer) - Handles all client-side communications with the server (including the builtin one) - forked
|
||||
|
|
|
|||
14
package.json
14
package.json
|
|
@ -5,7 +5,8 @@
|
|||
"scripts": {
|
||||
"dev-rsbuild": "rsbuild dev",
|
||||
"dev-proxy": "node server.js",
|
||||
"start": "run-p dev-rsbuild dev-proxy watch-mesher",
|
||||
"start": "run-p dev-proxy dev-rsbuild watch-mesher",
|
||||
"start2": "run-p dev-rsbuild watch-mesher",
|
||||
"build": "pnpm build-other-workers && rsbuild build",
|
||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
||||
"check-build": "tsx scripts/genShims.ts && tsc && pnpm build",
|
||||
|
|
@ -67,12 +68,12 @@
|
|||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"express": "^4.18.2",
|
||||
"filesize": "^10.0.12",
|
||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.47",
|
||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.49",
|
||||
"fs-extra": "^11.1.1",
|
||||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minecraft-data": "3.78.0",
|
||||
"minecraft-data": "3.80.0",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
||||
"mojangson": "^2.0.4",
|
||||
|
|
@ -92,6 +93,7 @@
|
|||
"react-dom": "^18.2.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-zoom-pan-pinch": "3.4.4",
|
||||
"remark": "^15.0.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"skinview3d": "^3.0.1",
|
||||
|
|
@ -140,7 +142,7 @@
|
|||
"http-browserify": "^1.7.0",
|
||||
"http-server": "^14.1.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mc-assets": "^0.2.12",
|
||||
"mc-assets": "^0.2.23",
|
||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||
"mineflayer": "github:zardoy/mineflayer",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
|
|
@ -172,7 +174,7 @@
|
|||
"diamond-square": "github:zardoy/diamond-square",
|
||||
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
||||
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
||||
"minecraft-data": "3.78.0",
|
||||
"minecraft-data": "3.80.0",
|
||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
||||
"prismarine-physics": "github:zardoy/prismarine-physics",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
|
|
@ -187,7 +189,7 @@
|
|||
"three@0.154.0": "patches/three@0.154.0.patch",
|
||||
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
|
||||
"mineflayer-item-map-downloader@1.2.0": "patches/mineflayer-item-map-downloader@1.2.0.patch",
|
||||
"minecraft-protocol@1.50.0": "patches/minecraft-protocol@1.49.0.patch"
|
||||
"minecraft-protocol@1.51.0": "patches/minecraft-protocol@1.49.0.patch"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.0.4"
|
||||
|
|
|
|||
223
pnpm-lock.yaml
generated
223
pnpm-lock.yaml
generated
|
|
@ -11,7 +11,7 @@ overrides:
|
|||
diamond-square: github:zardoy/diamond-square
|
||||
prismarine-block: github:zardoy/prismarine-block#next-era
|
||||
prismarine-world: github:zardoy/prismarine-world#next-era
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything
|
||||
prismarine-physics: github:zardoy/prismarine-physics
|
||||
minecraft-protocol: github:PrismarineJS/node-minecraft-protocol#master
|
||||
|
|
@ -20,7 +20,7 @@ overrides:
|
|||
prismarine-item: latest
|
||||
|
||||
patchedDependencies:
|
||||
minecraft-protocol@1.50.0:
|
||||
minecraft-protocol@1.51.0:
|
||||
hash: 7sh5krubuk2vjuogjioaktvwzi
|
||||
path: patches/minecraft-protocol@1.49.0.patch
|
||||
mineflayer-item-map-downloader@1.2.0:
|
||||
|
|
@ -119,8 +119,8 @@ importers:
|
|||
specifier: ^10.0.12
|
||||
version: 10.0.12
|
||||
flying-squid:
|
||||
specifier: npm:@zardoy/flying-squid@^0.0.47
|
||||
version: '@zardoy/flying-squid@0.0.47(encoding@0.1.13)'
|
||||
specifier: npm:@zardoy/flying-squid@^0.0.49
|
||||
version: '@zardoy/flying-squid@0.0.49(encoding@0.1.13)'
|
||||
fs-extra:
|
||||
specifier: ^11.1.1
|
||||
version: 11.1.1
|
||||
|
|
@ -134,11 +134,11 @@ importers:
|
|||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
minecraft-data:
|
||||
specifier: 3.78.0
|
||||
version: 3.78.0
|
||||
specifier: 3.80.0
|
||||
version: 3.80.0
|
||||
minecraft-protocol:
|
||||
specifier: github:PrismarineJS/node-minecraft-protocol#master
|
||||
version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/590dc33fed2100e77ef58e7db716dfc45eb61159(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(encoding@0.1.13)
|
||||
version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/f258c76b3a15badd902e82cd892168849444d79d(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(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/642fd4f7023a98a96da4caf8f993f8e19361a1e7(patch_hash=bck55yjvd4wrgz46x7o4vfur5q)(encoding@0.1.13)
|
||||
|
|
@ -147,7 +147,7 @@ importers:
|
|||
version: 2.0.4
|
||||
net-browserify:
|
||||
specifier: github:zardoy/prismarinejs-net-browserify
|
||||
version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/d3f7f77d8ac751bc171173bba639086c931a62f7
|
||||
version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/ab3721ca833308a0be099d14ea0053fbd8459ace
|
||||
node-gzip:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
|
|
@ -162,7 +162,7 @@ importers:
|
|||
version: 6.1.1
|
||||
prismarine-provider-anvil:
|
||||
specifier: github:zardoy/prismarine-provider-anvil#everything
|
||||
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/d807fc339a3d95a7aef91468d4d64d367e7c682a(minecraft-data@3.78.0)
|
||||
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/d807fc339a3d95a7aef91468d4d64d367e7c682a(minecraft-data@3.80.0)
|
||||
prosemirror-example-setup:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
|
|
@ -193,6 +193,9 @@ importers:
|
|||
react-transition-group:
|
||||
specifier: ^4.4.5
|
||||
version: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react-zoom-pan-pinch:
|
||||
specifier: 3.4.4
|
||||
version: 3.4.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
remark:
|
||||
specifier: ^15.0.1
|
||||
version: 15.0.1
|
||||
|
|
@ -343,14 +346,14 @@ importers:
|
|||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
mc-assets:
|
||||
specifier: ^0.2.12
|
||||
version: 0.2.12
|
||||
specifier: ^0.2.23
|
||||
version: 0.2.23
|
||||
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/ece6755d94931116924874d9f55bc024998cc1ae(encoding@0.1.13)
|
||||
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/390ce12c1e1f25e440a94ba422e45c874f8bbd2b(encoding@0.1.13)
|
||||
mineflayer-pathfinder:
|
||||
specifier: ^2.4.4
|
||||
version: 2.4.4
|
||||
|
|
@ -428,7 +431,7 @@ importers:
|
|||
version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/23849d4d24af91f45a5bd38781a6f82d40316c05
|
||||
prismarine-chunk:
|
||||
specifier: github:zardoy/prismarine-chunk#master
|
||||
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.78.0)
|
||||
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.80.0)
|
||||
prismarine-schematic:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.3
|
||||
|
|
@ -3041,6 +3044,9 @@ packages:
|
|||
'@types/node-fetch@2.6.6':
|
||||
resolution: {integrity: sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==}
|
||||
|
||||
'@types/node-rsa@1.1.4':
|
||||
resolution: {integrity: sha512-dB0ECel6JpMnq5ULvpUTunx3yNm8e/dIkv8Zu9p2c8me70xIRUUG3q+qXRwcSf9rN3oqamv4116iHy90dJGRpA==}
|
||||
|
||||
'@types/node@14.18.56':
|
||||
resolution: {integrity: sha512-+k+57NVS9opgrEn5l9c0gvD1r6C+PtyhVE4BTnMMRwiEA8ZO8uFcs6Yy2sXIy0eC95ZurBtRSvhZiHXBysbl6w==}
|
||||
|
||||
|
|
@ -3395,8 +3401,8 @@ packages:
|
|||
resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==}
|
||||
engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'}
|
||||
|
||||
'@zardoy/flying-squid@0.0.47':
|
||||
resolution: {integrity: sha512-VUtOqPGZ/20tQEjRLFpbz0taoTMi0GgoUM7002wn8RjuVmowg0pMUjdy0YwcFPGla8z1sOwjsF9cOtU4hQ8pUg==}
|
||||
'@zardoy/flying-squid@0.0.49':
|
||||
resolution: {integrity: sha512-Kt4wr5/R+44tcLU9gjuNG2an9weWeKEpIoKXfsgJN2GGQqdnbd5nBpxfGDdgZ9aMdFugsVW8BsyPZNhj9vbMXA==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -4870,10 +4876,6 @@ packages:
|
|||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
escalade@3.1.1:
|
||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
escalade@3.1.2:
|
||||
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -6580,8 +6582,8 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
|
||||
mc-assets@0.2.12:
|
||||
resolution: {integrity: sha512-ZbiodI0vgcwGT0M3AGc+0N2h7JsnrfjzhlA5AzpSQfkGbNp3wp/VeFmI4/lGm0JPJi9+LgXGDUuspRQzQwhobg==}
|
||||
mc-assets@0.2.23:
|
||||
resolution: {integrity: sha512-sLbPhsSOYdW8nYllIyPZbVPnLu7V3bZTgIO4mI4nlG525q17NIbUNEjItHKtdi60u0vI6qLgHKjf0CoNRqa/Nw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
md5-file@4.0.0:
|
||||
|
|
@ -6770,8 +6772,8 @@ packages:
|
|||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
minecraft-data@3.78.0:
|
||||
resolution: {integrity: sha512-Ssks8QD31lsoxqa7LySTqeP9romsfAbfsSGiUHiGMeqfxRi/PtOxGLyKD1BXB8V/tXLztFcbQYqzIhprDkPguw==}
|
||||
minecraft-data@3.80.0:
|
||||
resolution: {integrity: sha512-UYq+ADpS9K1+cqiJiz6tqkht4y4cRYF3qOYanG9eIiHY+VC+qIAC7/UcW6G3adayvj5YBOCurlqaw3E0TMAtHg==}
|
||||
|
||||
minecraft-folder-path@1.2.0:
|
||||
resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==}
|
||||
|
|
@ -6780,9 +6782,9 @@ 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/590dc33fed2100e77ef58e7db716dfc45eb61159:
|
||||
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/590dc33fed2100e77ef58e7db716dfc45eb61159}
|
||||
version: 1.50.0
|
||||
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/f258c76b3a15badd902e82cd892168849444d79d:
|
||||
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/f258c76b3a15badd902e82cd892168849444d79d}
|
||||
version: 1.51.0
|
||||
engines: {node: '>=14'}
|
||||
|
||||
minecraft-wrap@1.5.1:
|
||||
|
|
@ -6803,8 +6805,8 @@ packages:
|
|||
resolution: {integrity: sha512-wSchhS59hK+oPs8tFg847H82YEvxU7zYKdDKj4e5FVo3CxJ74eXJVT+JcFwEvoqFO7kXiQlhJITxEvO13GOSKA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/ece6755d94931116924874d9f55bc024998cc1ae:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/ece6755d94931116924874d9f55bc024998cc1ae}
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/390ce12c1e1f25e440a94ba422e45c874f8bbd2b:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/390ce12c1e1f25e440a94ba422e45c874f8bbd2b}
|
||||
version: 4.23.0
|
||||
engines: {node: '>=18'}
|
||||
|
||||
|
|
@ -6969,8 +6971,8 @@ packages:
|
|||
neo-async@2.6.2:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
|
||||
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/d3f7f77d8ac751bc171173bba639086c931a62f7:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/d3f7f77d8ac751bc171173bba639086c931a62f7}
|
||||
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/ab3721ca833308a0be099d14ea0053fbd8459ace:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/ab3721ca833308a0be099d14ea0053fbd8459ace}
|
||||
version: 0.2.4
|
||||
|
||||
nice-try@1.0.5:
|
||||
|
|
@ -7516,7 +7518,7 @@ packages:
|
|||
prismarine-biome@1.3.0:
|
||||
resolution: {integrity: sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==}
|
||||
peerDependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-registry: ^1.1.0
|
||||
|
||||
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/23849d4d24af91f45a5bd38781a6f82d40316c05:
|
||||
|
|
@ -7931,6 +7933,13 @@ packages:
|
|||
react: ^18.2.0
|
||||
react-dom: ^16.8.0 || ^17.0.0
|
||||
|
||||
react-zoom-pan-pinch@3.4.4:
|
||||
resolution: {integrity: sha512-lGTu7D9lQpYEQ6sH+NSlLA7gicgKRW8j+D/4HO1AbSV2POvKRFzdWQ8eI0r3xmOsl4dYQcY+teV6MhULeg1xBw==}
|
||||
engines: {node: '>=8', npm: '>=5'}
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
react-dom: '*'
|
||||
|
||||
react@18.2.0:
|
||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -9853,7 +9862,7 @@ snapshots:
|
|||
'@babel/core': 7.22.11
|
||||
'@babel/helper-compilation-targets': 7.22.10
|
||||
'@babel/helper-plugin-utils': 7.22.5
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
lodash.debounce: 4.0.8
|
||||
resolve: 1.22.4
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -10580,7 +10589,7 @@ snapshots:
|
|||
'@babel/helper-split-export-declaration': 7.22.6
|
||||
'@babel/parser': 7.22.13
|
||||
'@babel/types': 7.23.0
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -11117,7 +11126,7 @@ snapshots:
|
|||
'@jest/schemas': 29.6.3
|
||||
'@types/istanbul-lib-coverage': 2.0.4
|
||||
'@types/istanbul-reports': 3.0.2
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
'@types/yargs': 17.0.28
|
||||
chalk: 4.1.2
|
||||
|
||||
|
|
@ -12823,7 +12832,7 @@ snapshots:
|
|||
'@types/body-parser@1.19.3':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.36
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/chai-subset@1.3.3':
|
||||
dependencies:
|
||||
|
|
@ -12833,17 +12842,17 @@ snapshots:
|
|||
|
||||
'@types/connect@3.4.36':
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/cookie@0.4.1': {}
|
||||
|
||||
'@types/cors@2.8.15':
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/cross-spawn@6.0.3':
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
|
|
@ -12883,7 +12892,7 @@ snapshots:
|
|||
|
||||
'@types/express-serve-static-core@4.17.37':
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
'@types/qs': 6.9.8
|
||||
'@types/range-parser': 1.2.5
|
||||
'@types/send': 0.17.2
|
||||
|
|
@ -12902,11 +12911,11 @@ snapshots:
|
|||
'@types/glob@7.2.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/graceful-fs@4.1.7':
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/http-cache-semantics@4.0.2': {}
|
||||
|
||||
|
|
@ -12954,9 +12963,13 @@ snapshots:
|
|||
|
||||
'@types/node-fetch@2.6.6':
|
||||
dependencies:
|
||||
'@types/node': 20.8.0
|
||||
'@types/node': 22.8.1
|
||||
form-data: 4.0.0
|
||||
|
||||
'@types/node-rsa@1.1.4':
|
||||
dependencies:
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/node@14.18.56':
|
||||
optional: true
|
||||
|
||||
|
|
@ -12971,7 +12984,6 @@ snapshots:
|
|||
'@types/node@22.8.1':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
optional: true
|
||||
|
||||
'@types/normalize-package-data@2.4.2': {}
|
||||
|
||||
|
|
@ -13012,7 +13024,7 @@ snapshots:
|
|||
|
||||
'@types/resolve@1.17.1':
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/sat@0.0.31': {}
|
||||
|
||||
|
|
@ -13023,13 +13035,13 @@ snapshots:
|
|||
'@types/send@0.17.2':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.3
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/serve-static@1.15.3':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.2
|
||||
'@types/mime': 3.0.2
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/sinonjs__fake-timers@8.1.1':
|
||||
optional: true
|
||||
|
|
@ -13079,11 +13091,11 @@ snapshots:
|
|||
|
||||
'@types/yauzl@2.10.1':
|
||||
dependencies:
|
||||
'@types/node': 20.8.0
|
||||
'@types/node': 22.8.1
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/eslint-plugin@6.1.0(@typescript-eslint/parser@6.7.3(eslint@8.50.0)(typescript@5.5.4))(eslint@8.50.0)(typescript@5.5.4)':
|
||||
|
|
@ -13139,7 +13151,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 6.1.0(typescript@5.5.4)
|
||||
'@typescript-eslint/utils': 6.1.0(eslint@8.50.0)(typescript@5.5.4)
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
eslint: 8.50.0
|
||||
ts-api-utils: 1.0.3(typescript@5.5.4)
|
||||
optionalDependencies:
|
||||
|
|
@ -13157,7 +13169,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 6.1.0
|
||||
'@typescript-eslint/visitor-keys': 6.1.0
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.6.0
|
||||
|
|
@ -13171,7 +13183,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 6.7.3
|
||||
'@typescript-eslint/visitor-keys': 6.7.3
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.6.0
|
||||
|
|
@ -13185,7 +13197,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 8.0.0
|
||||
'@typescript-eslint/visitor-keys': 8.0.0
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
|
|
@ -13419,7 +13431,7 @@ snapshots:
|
|||
'@types/emscripten': 1.39.8
|
||||
tslib: 1.14.1
|
||||
|
||||
'@zardoy/flying-squid@0.0.47(encoding@0.1.13)':
|
||||
'@zardoy/flying-squid@0.0.49(encoding@0.1.13)':
|
||||
dependencies:
|
||||
'@tootallnate/once': 2.0.0
|
||||
chalk: 5.3.0
|
||||
|
|
@ -13429,16 +13441,16 @@ snapshots:
|
|||
exit-hook: 2.2.1
|
||||
flatmap: 0.0.3
|
||||
long: 5.2.3
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/590dc33fed2100e77ef58e7db716dfc45eb61159(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(encoding@0.1.13)
|
||||
minecraft-data: 3.80.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/f258c76b3a15badd902e82cd892168849444d79d(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(encoding@0.1.13)
|
||||
mkdirp: 2.1.6
|
||||
node-gzip: 1.1.2
|
||||
node-rsa: 1.1.1
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.78.0)
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.80.0)
|
||||
prismarine-entity: 2.3.1
|
||||
prismarine-item: 1.15.0
|
||||
prismarine-nbt: 2.5.0
|
||||
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/d807fc339a3d95a7aef91468d4d64d367e7c682a(minecraft-data@3.78.0)
|
||||
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/d807fc339a3d95a7aef91468d4d64d367e7c682a(minecraft-data@3.80.0)
|
||||
prismarine-windows: 2.9.0
|
||||
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/6221e049e2ad0f508edc23c7f5bda7fd6d9566be
|
||||
rambda: 9.2.0
|
||||
|
|
@ -14912,7 +14924,7 @@ snapshots:
|
|||
detect-port@1.5.1:
|
||||
dependencies:
|
||||
address: 1.2.2
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -14922,8 +14934,8 @@ snapshots:
|
|||
|
||||
diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71:
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.78.0)
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.80.0)
|
||||
prismarine-registry: 1.10.0
|
||||
random-seed: 0.3.0
|
||||
vec3: 0.1.8
|
||||
|
|
@ -15127,7 +15139,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/cookie': 0.4.1
|
||||
'@types/cors': 2.8.15
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.4.2
|
||||
|
|
@ -15418,8 +15430,6 @@ snapshots:
|
|||
'@esbuild/win32-ia32': 0.19.3
|
||||
'@esbuild/win32-x64': 0.19.3
|
||||
|
||||
escalade@3.1.1: {}
|
||||
|
||||
escalade@3.1.2: {}
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
|
|
@ -16567,7 +16577,7 @@ snapshots:
|
|||
https-proxy-agent@4.0.0:
|
||||
dependencies:
|
||||
agent-base: 5.1.1
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -16582,7 +16592,7 @@ snapshots:
|
|||
https-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.0
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -16978,7 +16988,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/graceful-fs': 4.1.7
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
anymatch: 3.1.3
|
||||
fb-watchman: 2.0.2
|
||||
graceful-fs: 4.2.11
|
||||
|
|
@ -16995,7 +17005,7 @@ snapshots:
|
|||
jest-util@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
graceful-fs: 4.2.11
|
||||
|
|
@ -17003,19 +17013,19 @@ snapshots:
|
|||
|
||||
jest-worker@26.6.2:
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
jest-worker@29.7.0:
|
||||
dependencies:
|
||||
'@types/node': 20.12.8
|
||||
'@types/node': 22.8.1
|
||||
jest-util: 29.7.0
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
|
@ -17443,7 +17453,7 @@ snapshots:
|
|||
dependencies:
|
||||
react: 18.2.0
|
||||
|
||||
mc-assets@0.2.12: {}
|
||||
mc-assets@0.2.23: {}
|
||||
|
||||
md5-file@4.0.0: {}
|
||||
|
||||
|
|
@ -17656,7 +17666,7 @@ snapshots:
|
|||
micromark@4.0.0:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
decode-named-character-reference: 1.0.2
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.0
|
||||
|
|
@ -17727,7 +17737,7 @@ snapshots:
|
|||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
minecraft-data@3.78.0: {}
|
||||
minecraft-data@3.80.0: {}
|
||||
|
||||
minecraft-folder-path@1.2.0: {}
|
||||
|
||||
|
|
@ -17738,8 +17748,9 @@ snapshots:
|
|||
- '@types/react'
|
||||
- react
|
||||
|
||||
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/590dc33fed2100e77ef58e7db716dfc45eb61159(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(encoding@0.1.13):
|
||||
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/f258c76b3a15badd902e82cd892168849444d79d(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@types/node-rsa': 1.1.4
|
||||
'@types/readable-stream': 4.0.12
|
||||
aes-js: 3.1.2
|
||||
buffer-equal: 1.0.1
|
||||
|
|
@ -17747,7 +17758,7 @@ snapshots:
|
|||
endian-toggle: 0.0.0
|
||||
lodash.get: 4.4.2
|
||||
lodash.merge: 4.6.2
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
minecraft-folder-path: 1.2.0
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
node-rsa: 0.4.2
|
||||
|
|
@ -17796,7 +17807,7 @@ snapshots:
|
|||
|
||||
mineflayer-pathfinder@2.4.4:
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/23849d4d24af91f45a5bd38781a6f82d40316c05
|
||||
prismarine-entity: 2.3.1
|
||||
prismarine-item: 1.15.0
|
||||
|
|
@ -17806,12 +17817,12 @@ snapshots:
|
|||
|
||||
mineflayer@4.23.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/590dc33fed2100e77ef58e7db716dfc45eb61159(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(encoding@0.1.13)
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.78.0)(prismarine-registry@1.10.0)
|
||||
minecraft-data: 3.80.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/f258c76b3a15badd902e82cd892168849444d79d(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(encoding@0.1.13)
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.80.0)(prismarine-registry@1.10.0)
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/23849d4d24af91f45a5bd38781a6f82d40316c05
|
||||
prismarine-chat: 1.10.1
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.78.0)
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.80.0)
|
||||
prismarine-entity: 2.3.1
|
||||
prismarine-item: 1.15.0
|
||||
prismarine-nbt: 2.5.0
|
||||
|
|
@ -17827,14 +17838,14 @@ snapshots:
|
|||
- encoding
|
||||
- supports-color
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/ece6755d94931116924874d9f55bc024998cc1ae(encoding@0.1.13):
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/390ce12c1e1f25e440a94ba422e45c874f8bbd2b(encoding@0.1.13):
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/590dc33fed2100e77ef58e7db716dfc45eb61159(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(encoding@0.1.13)
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.78.0)(prismarine-registry@1.10.0)
|
||||
minecraft-data: 3.80.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/f258c76b3a15badd902e82cd892168849444d79d(patch_hash=7sh5krubuk2vjuogjioaktvwzi)(encoding@0.1.13)
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.80.0)(prismarine-registry@1.10.0)
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/23849d4d24af91f45a5bd38781a6f82d40316c05
|
||||
prismarine-chat: 1.10.1
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.78.0)
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.80.0)
|
||||
prismarine-entity: 2.3.1
|
||||
prismarine-item: 1.15.0
|
||||
prismarine-nbt: 2.5.0
|
||||
|
|
@ -18028,7 +18039,7 @@ snapshots:
|
|||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/d3f7f77d8ac751bc171173bba639086c931a62f7:
|
||||
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/ab3721ca833308a0be099d14ea0053fbd8459ace:
|
||||
dependencies:
|
||||
body-parser: 1.20.2
|
||||
express: 4.18.2
|
||||
|
|
@ -18630,20 +18641,20 @@ snapshots:
|
|||
- encoding
|
||||
- supports-color
|
||||
|
||||
prismarine-biome@1.3.0(minecraft-data@3.78.0)(prismarine-registry@1.10.0):
|
||||
prismarine-biome@1.3.0(minecraft-data@3.80.0)(prismarine-registry@1.10.0):
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-registry: 1.10.0
|
||||
|
||||
prismarine-biome@1.3.0(minecraft-data@3.78.0)(prismarine-registry@1.7.0):
|
||||
prismarine-biome@1.3.0(minecraft-data@3.80.0)(prismarine-registry@1.7.0):
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-registry: 1.7.0
|
||||
|
||||
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/23849d4d24af91f45a5bd38781a6f82d40316c05:
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.78.0)(prismarine-registry@1.7.0)
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.80.0)(prismarine-registry@1.7.0)
|
||||
prismarine-chat: 1.10.1
|
||||
prismarine-item: 1.15.0
|
||||
prismarine-nbt: 2.5.0
|
||||
|
|
@ -18655,9 +18666,9 @@ snapshots:
|
|||
prismarine-nbt: 2.5.0
|
||||
prismarine-registry: 1.10.0
|
||||
|
||||
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.78.0):
|
||||
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.80.0):
|
||||
dependencies:
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.78.0)(prismarine-registry@1.10.0)
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.80.0)(prismarine-registry@1.10.0)
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/23849d4d24af91f45a5bd38781a6f82d40316c05
|
||||
prismarine-nbt: 2.5.0
|
||||
prismarine-registry: 1.10.0
|
||||
|
|
@ -18690,14 +18701,14 @@ snapshots:
|
|||
|
||||
prismarine-physics@https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b:
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-nbt: 2.5.0
|
||||
vec3: 0.1.8
|
||||
|
||||
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/d807fc339a3d95a7aef91468d4d64d367e7c682a(minecraft-data@3.78.0):
|
||||
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/d807fc339a3d95a7aef91468d4d64d367e7c682a(minecraft-data@3.80.0):
|
||||
dependencies:
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/23849d4d24af91f45a5bd38781a6f82d40316c05
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.78.0)
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/416dd49bec42f4cc9f50ccf79527e6e4c01cebcb(minecraft-data@3.80.0)
|
||||
prismarine-nbt: 2.5.0
|
||||
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/6221e049e2ad0f508edc23c7f5bda7fd6d9566be
|
||||
uint4: 0.1.2
|
||||
|
|
@ -18719,17 +18730,17 @@ snapshots:
|
|||
|
||||
prismarine-registry@1.10.0:
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-nbt: 2.5.0
|
||||
|
||||
prismarine-registry@1.7.0:
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-nbt: 2.5.0
|
||||
|
||||
prismarine-schematic@1.2.3:
|
||||
dependencies:
|
||||
minecraft-data: 3.78.0
|
||||
minecraft-data: 3.80.0
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/23849d4d24af91f45a5bd38781a6f82d40316c05
|
||||
prismarine-nbt: 2.2.1
|
||||
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/6221e049e2ad0f508edc23c7f5bda7fd6d9566be
|
||||
|
|
@ -19181,6 +19192,11 @@ snapshots:
|
|||
ts-easing: 0.2.0
|
||||
tslib: 2.6.2
|
||||
|
||||
react-zoom-pan-pinch@3.4.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
react@18.2.0:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
|
@ -20577,8 +20593,7 @@ snapshots:
|
|||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
undici-types@6.19.8:
|
||||
optional: true
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
undici@5.25.4:
|
||||
dependencies:
|
||||
|
|
@ -20698,7 +20713,7 @@ snapshots:
|
|||
update-browserslist-db@1.0.11(browserslist@4.21.10):
|
||||
dependencies:
|
||||
browserslist: 4.21.10
|
||||
escalade: 3.1.1
|
||||
escalade: 3.1.2
|
||||
picocolors: 1.0.1
|
||||
|
||||
update-browserslist-db@1.1.0(browserslist@4.23.2):
|
||||
|
|
@ -21310,7 +21325,7 @@ snapshots:
|
|||
yargs@16.2.0:
|
||||
dependencies:
|
||||
cliui: 7.0.4
|
||||
escalade: 3.1.1
|
||||
escalade: 3.1.2
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { WorldDataEmitter } from '../viewer'
|
|||
import { Viewer } from '../viewer/lib/viewer'
|
||||
import { BlockNames } from '../../src/mcDataTypes'
|
||||
import { initWithRenderer, statsEnd, statsStart } from '../../src/topRightStats'
|
||||
import { defaultWorldRendererConfig } from '../viewer/lib/worldrendererCommon'
|
||||
import { getSyncWorld } from './shared'
|
||||
|
||||
window.THREE = THREE
|
||||
|
|
@ -158,7 +159,7 @@ export class BasePlaygroundScene {
|
|||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
|
||||
// Create viewer
|
||||
const viewer = new Viewer(renderer, { numWorkers: 6, showChunkBorders: false, })
|
||||
const viewer = new Viewer(renderer, { ...defaultWorldRendererConfig, numWorkers: 6 })
|
||||
window.viewer = viewer
|
||||
const isWebgpu = false
|
||||
const promises = [] as Array<Promise<void>>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ import { proxy, useSnapshot } from 'valtio'
|
|||
import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface'
|
||||
import { css } from '@emotion/css'
|
||||
import { Vec3 } from 'vec3'
|
||||
import useLongPress from '../../src/react/useLongPress'
|
||||
import { isMobile } from '../viewer/lib/simpleUtils'
|
||||
|
||||
export const playgroundGlobalUiState = proxy({
|
||||
scenes: [] as string[],
|
||||
selected: ''
|
||||
selected: '',
|
||||
selectorOpened: false,
|
||||
actions: {} as Record<string, () => void>,
|
||||
})
|
||||
|
||||
renderToDom(<Playground />)
|
||||
|
|
@ -17,7 +21,7 @@ function Playground () {
|
|||
const style = document.createElement('style')
|
||||
style.innerHTML = /* css */ `
|
||||
.lil-gui {
|
||||
top: 40px !important;
|
||||
top: 60px !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
`
|
||||
|
|
@ -33,24 +37,31 @@ function Playground () {
|
|||
}}>
|
||||
<Controls />
|
||||
<SceneSelector />
|
||||
<ActionsSelector />
|
||||
</div>
|
||||
}
|
||||
|
||||
function SceneSelector () {
|
||||
const mobile = isMobile()
|
||||
const { scenes, selected } = useSnapshot(playgroundGlobalUiState)
|
||||
const longPressEvents = useLongPress(() => {
|
||||
playgroundGlobalUiState.selectorOpened = true
|
||||
}, () => { })
|
||||
|
||||
return <div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}>
|
||||
return <div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
}} {...longPressEvents}>
|
||||
{scenes.map(scene => <div
|
||||
key={scene}
|
||||
style={{
|
||||
padding: '2px 5px',
|
||||
padding: mobile ? '5px' : '2px 5px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
background: scene === selected ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.6)',
|
||||
fontWeight: scene === selected ? 'bold' : 'normal',
|
||||
}}
|
||||
onClick={() => {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
|
|
@ -61,6 +72,41 @@ function SceneSelector () {
|
|||
</div>
|
||||
}
|
||||
|
||||
const ActionsSelector = () => {
|
||||
const { actions, selectorOpened } = useSnapshot(playgroundGlobalUiState)
|
||||
|
||||
if (!selectorOpened) return null
|
||||
return <div style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
fontSize: 24,
|
||||
}}>{Object.entries({
|
||||
...actions,
|
||||
'Close' () {
|
||||
playgroundGlobalUiState.selectorOpened = false
|
||||
}
|
||||
}).map(([name, action]) => <div
|
||||
key={name}
|
||||
style={{
|
||||
padding: '2px 5px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
}}
|
||||
onClick={() => {
|
||||
action()
|
||||
playgroundGlobalUiState.selectorOpened = false
|
||||
}}
|
||||
>{name}</div>)}</div>
|
||||
}
|
||||
|
||||
const Controls = () => {
|
||||
// todo setting
|
||||
const usingTouch = navigator.maxTouchPoints > 0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//@ts-check
|
||||
import EventEmitter from 'events'
|
||||
import { UnionToIntersection } from 'type-fest'
|
||||
import nbt from 'prismarine-nbt'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import * as THREE from 'three'
|
||||
|
|
@ -11,6 +12,7 @@ import { NameTagObject } from 'skinview3d/libs/nametag'
|
|||
import { flat, fromFormattedString } from '@xmcl/text-component'
|
||||
import mojangson from 'mojangson'
|
||||
import { snakeCase } from 'change-case'
|
||||
import { EntityMetadataVersions } from '../../../src/mcDataTypes'
|
||||
import * as Entity from './entity/EntityMesh'
|
||||
import { WalkingGeneralSwing } from './entity/animations'
|
||||
import externalTexturesJson from './entity/externalTextures.json'
|
||||
|
|
@ -20,12 +22,51 @@ export const TWEEN_DURATION = 120
|
|||
|
||||
type PlayerObjectType = PlayerObject & { animation?: PlayerAnimation }
|
||||
|
||||
function getUsernameTexture (username: string, { fontFamily = 'sans-serif' }: any) {
|
||||
function convert2sComplementToHex (complement: number) {
|
||||
if (complement < 0) {
|
||||
complement = (0xFF_FF_FF_FF + complement + 1) >>> 0
|
||||
}
|
||||
return complement.toString(16)
|
||||
}
|
||||
|
||||
function toRgba (color: string | undefined) {
|
||||
if (color === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (parseInt(color, 10) === 0) {
|
||||
return 'rgba(0, 0, 0, 0)'
|
||||
}
|
||||
const hex = convert2sComplementToHex(parseInt(color, 10))
|
||||
if (hex.length === 8) {
|
||||
return `#${hex.slice(2, 8)}${hex.slice(0, 2)}`
|
||||
} else {
|
||||
return `#${hex}`
|
||||
}
|
||||
}
|
||||
|
||||
function toQuaternion (quaternion: any, defaultValue?: THREE.Quaternion) {
|
||||
if (quaternion === undefined) {
|
||||
return defaultValue
|
||||
}
|
||||
if (quaternion instanceof THREE.Quaternion) {
|
||||
return quaternion
|
||||
}
|
||||
if (Array.isArray(quaternion)) {
|
||||
return new THREE.Quaternion(quaternion[0], quaternion[1], quaternion[2], quaternion[3])
|
||||
}
|
||||
return new THREE.Quaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w)
|
||||
}
|
||||
|
||||
function getUsernameTexture ({
|
||||
username,
|
||||
nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)',
|
||||
nameTagTextOpacity = 255
|
||||
}: any, { fontFamily = 'sans-serif' }: any) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Could not get 2d context')
|
||||
|
||||
const fontSize = 50
|
||||
const fontSize = 48
|
||||
const padding = 5
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
|
||||
|
|
@ -38,17 +79,17 @@ function getUsernameTexture (username: string, { fontFamily = 'sans-serif' }: an
|
|||
}
|
||||
|
||||
canvas.width = textWidth
|
||||
canvas.height = (fontSize + padding * 2) * lines.length
|
||||
canvas.height = (fontSize + padding) * lines.length
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
|
||||
ctx.fillStyle = nameTagBackgroundColor
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${nameTagTextOpacity / 255})`
|
||||
let i = 0
|
||||
for (const line of lines) {
|
||||
i++
|
||||
ctx.fillText(line, padding + (textWidth - ctx.measureText(line).width) / 2, fontSize * i)
|
||||
ctx.fillText(line, (textWidth - ctx.measureText(line).width) / 2, -padding + fontSize * i)
|
||||
}
|
||||
|
||||
return canvas
|
||||
|
|
@ -57,17 +98,39 @@ function getUsernameTexture (username: string, { fontFamily = 'sans-serif' }: an
|
|||
const addNametag = (entity, options, mesh) => {
|
||||
if (entity.username !== undefined) {
|
||||
if (mesh.children.some(c => c.name === 'nametag')) return // todo update
|
||||
const canvas = getUsernameTexture(entity.username, options)
|
||||
const canvas = getUsernameTexture(entity, options)
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.needsUpdate = true
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: tex })
|
||||
const sprite = new THREE.Sprite(spriteMat)
|
||||
sprite.renderOrder = 1000
|
||||
sprite.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
|
||||
sprite.position.y += entity.height + 0.6
|
||||
sprite.name = 'nametag'
|
||||
let nameTag
|
||||
if (entity.nameTagFixed) {
|
||||
const geometry = new THREE.PlaneGeometry()
|
||||
const material = new THREE.MeshBasicMaterial({ map: tex })
|
||||
material.transparent = true
|
||||
nameTag = new THREE.Mesh(geometry, material)
|
||||
nameTag.rotation.set(entity.pitch, THREE.MathUtils.degToRad(entity.yaw + 180), 0)
|
||||
nameTag.position.y += entity.height + 0.3
|
||||
} else {
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: tex })
|
||||
nameTag = new THREE.Sprite(spriteMat)
|
||||
nameTag.position.y += entity.height + 0.6
|
||||
}
|
||||
nameTag.renderOrder = 1000
|
||||
nameTag.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
|
||||
if (entity.nameTagRotationRight) {
|
||||
nameTag.applyQuaternion(entity.nameTagRotationRight)
|
||||
}
|
||||
if (entity.nameTagScale) {
|
||||
nameTag.scale.multiply(entity.nameTagScale)
|
||||
}
|
||||
if (entity.nameTagRotationLeft) {
|
||||
nameTag.applyQuaternion(entity.nameTagRotationLeft)
|
||||
}
|
||||
if (entity.nameTagTranslation) {
|
||||
nameTag.position.add(entity.nameTagTranslation)
|
||||
}
|
||||
nameTag.name = 'nametag'
|
||||
|
||||
mesh.add(sprite)
|
||||
mesh.add(nameTag)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -302,6 +365,9 @@ export class Entities extends EventEmitter {
|
|||
parseEntityLabel (jsonLike) {
|
||||
if (!jsonLike) return
|
||||
try {
|
||||
if (jsonLike.type === 'string') {
|
||||
return jsonLike.value
|
||||
}
|
||||
const parsed = typeof jsonLike === 'string' ? mojangson.simplify(mojangson.parse(jsonLike)) : nbt.simplify(jsonLike)
|
||||
const text = flat(parsed).map(x => x.text)
|
||||
return text.join('')
|
||||
|
|
@ -352,7 +418,7 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
update (entity: import('prismarine-entity').Entity & { delete?; pos }, overrides) {
|
||||
update (entity: import('prismarine-entity').Entity & { delete?; pos, name }, overrides) {
|
||||
const isPlayerModel = entity.name === 'player'
|
||||
if (entity.name === 'zombie' || entity.name === 'zombie_villager' || entity.name === 'husk') {
|
||||
overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}`
|
||||
|
|
@ -453,6 +519,8 @@ export class Entities extends EventEmitter {
|
|||
this.setRendering(this.rendering, group)
|
||||
}
|
||||
|
||||
const meta = getGeneralEntitiesMetadata(entity)
|
||||
|
||||
//@ts-expect-error
|
||||
// set visibility
|
||||
const isInvisible = entity.metadata?.[0] & 0x20
|
||||
|
|
@ -463,10 +531,24 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
// ---
|
||||
// not player
|
||||
const displayText = entity.metadata?.[3] && this.parseEntityLabel(entity.metadata[2])
|
||||
|| entity.metadata?.[23] && this.parseEntityLabel(entity.metadata[23]) // text displays
|
||||
const textDisplayMeta = getSpecificEntityMetadata('text_display', entity)
|
||||
const displayTextRaw = textDisplayMeta?.text || meta.custom_name_visible && meta.custom_name
|
||||
const displayText = this.parseEntityLabel(displayTextRaw)
|
||||
if (entity.name !== 'player' && displayText) {
|
||||
addNametag({ ...entity, username: displayText }, this.entitiesOptions, this.entities[entity.id].children.find(c => c.name === 'mesh'))
|
||||
const nameTagFixed = textDisplayMeta && (textDisplayMeta.billboard_render_constraints === 'fixed' || !textDisplayMeta.billboard_render_constraints)
|
||||
const nameTagBackgroundColor = textDisplayMeta && toRgba(textDisplayMeta.background_color)
|
||||
let nameTagTextOpacity: any
|
||||
if (textDisplayMeta?.text_opacity) {
|
||||
const rawOpacity = parseInt(textDisplayMeta?.text_opacity, 10)
|
||||
nameTagTextOpacity = rawOpacity > 0 ? rawOpacity : 256 - rawOpacity
|
||||
}
|
||||
addNametag(
|
||||
{ ...entity, username: displayText, nameTagBackgroundColor, nameTagTextOpacity, nameTagFixed,
|
||||
nameTagScale: textDisplayMeta?.scale, nameTagTranslation: textDisplayMeta && (textDisplayMeta.translation || new THREE.Vector3(0, 0, 0)),
|
||||
nameTagRotationLeft: toQuaternion(textDisplayMeta?.left_rotation), nameTagRotationRight: toQuaternion(textDisplayMeta?.right_rotation) },
|
||||
this.entitiesOptions,
|
||||
this.entities[entity.id].children.find(c => c.name === 'mesh')
|
||||
)
|
||||
}
|
||||
|
||||
// todo handle map, map_chunks events
|
||||
|
|
@ -547,3 +629,19 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getGeneralEntitiesMetadata (entity: { name; metadata }): Partial<UnionToIntersection<EntityMetadataVersions[keyof EntityMetadataVersions]>> {
|
||||
const entityData = loadedData.entitiesByName[entity.name]
|
||||
return new Proxy({}, {
|
||||
get (target, p, receiver) {
|
||||
if (typeof p !== 'string' || !entityData) return
|
||||
const index = entityData.metadataKeys?.indexOf(p)
|
||||
return entity.metadata[index ?? -1]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function getSpecificEntityMetadata<T extends keyof EntityMetadataVersions> (name: T, entity): EntityMetadataVersions[T] | undefined {
|
||||
if (entity.name !== name) return
|
||||
return getGeneralEntitiesMetadata(entity) as any
|
||||
}
|
||||
|
|
|
|||
96
prismarine-viewer/viewer/lib/hand.ts
Normal file
96
prismarine-viewer/viewer/lib/hand.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import * as THREE from 'three'
|
||||
import { loadSkinToCanvas } from 'skinview-utils'
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
|
||||
let steveTexture: THREE.Texture
|
||||
export const getMyHand = async (image?: string) => {
|
||||
let newMap: THREE.Texture
|
||||
if (!image && steveTexture) {
|
||||
newMap = steveTexture
|
||||
} else {
|
||||
image ??= stevePng
|
||||
const skinCanvas = document.createElement('canvas')
|
||||
const img = new Image()
|
||||
img.src = image
|
||||
await new Promise<void>(resolve => {
|
||||
img.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
loadSkinToCanvas(skinCanvas, img)
|
||||
newMap = new THREE.CanvasTexture(skinCanvas)
|
||||
// newMap.flipY = false
|
||||
newMap.magFilter = THREE.NearestFilter
|
||||
newMap.minFilter = THREE.NearestFilter
|
||||
if (!image) {
|
||||
steveTexture = newMap
|
||||
}
|
||||
}
|
||||
|
||||
// right arm
|
||||
const box = new THREE.BoxGeometry()
|
||||
const material = new THREE.MeshStandardMaterial()
|
||||
const slim = false
|
||||
const mesh = new THREE.Mesh(box, material)
|
||||
mesh.scale.x = slim ? 3 : 4
|
||||
mesh.scale.y = 12
|
||||
mesh.scale.z = 4
|
||||
setSkinUVs(box, 40, 16, slim ? 3 : 4, 12, 4)
|
||||
material.map = newMap
|
||||
material.needsUpdate = true
|
||||
const group = new THREE.Group()
|
||||
group.add(mesh)
|
||||
group.scale.set(0.1, 0.1, 0.1)
|
||||
mesh.rotation.z = Math.PI
|
||||
return group
|
||||
}
|
||||
|
||||
function setUVs (
|
||||
box: THREE.BoxGeometry,
|
||||
u: number,
|
||||
v: number,
|
||||
width: number,
|
||||
height: number,
|
||||
depth: number,
|
||||
textureWidth: number,
|
||||
textureHeight: number
|
||||
): void {
|
||||
const toFaceVertices = (x1: number, y1: number, x2: number, y2: number) => [
|
||||
new THREE.Vector2(x1 / textureWidth, 1 - y2 / textureHeight),
|
||||
new THREE.Vector2(x2 / textureWidth, 1 - y2 / textureHeight),
|
||||
new THREE.Vector2(x2 / textureWidth, 1 - y1 / textureHeight),
|
||||
new THREE.Vector2(x1 / textureWidth, 1 - y1 / textureHeight),
|
||||
]
|
||||
|
||||
const top = toFaceVertices(u + depth, v, u + width + depth, v + depth)
|
||||
const bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth)
|
||||
const left = toFaceVertices(u, v + depth, u + depth, v + depth + height)
|
||||
const front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height)
|
||||
const right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth)
|
||||
const back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth)
|
||||
|
||||
const uvAttr = box.attributes.uv as THREE.BufferAttribute
|
||||
const uvRight = [right[3], right[2], right[0], right[1]]
|
||||
const uvLeft = [left[3], left[2], left[0], left[1]]
|
||||
const uvTop = [top[3], top[2], top[0], top[1]]
|
||||
const uvBottom = [bottom[0], bottom[1], bottom[3], bottom[2]]
|
||||
const uvFront = [front[3], front[2], front[0], front[1]]
|
||||
const uvBack = [back[3], back[2], back[0], back[1]]
|
||||
|
||||
// Create a new array to hold the modified UV data
|
||||
const newUVData = [] as number[]
|
||||
|
||||
// Iterate over the arrays and copy the data to uvData
|
||||
for (const uvArray of [uvRight, uvLeft, uvTop, uvBottom, uvFront, uvBack]) {
|
||||
for (const uv of uvArray) {
|
||||
newUVData.push(uv.x, uv.y)
|
||||
}
|
||||
}
|
||||
|
||||
uvAttr.set(new Float32Array(newUVData))
|
||||
uvAttr.needsUpdate = true
|
||||
}
|
||||
|
||||
function setSkinUVs (box: THREE.BoxGeometry, u: number, v: number, width: number, height: number, depth: number): void {
|
||||
setUVs(box, u, v, width, height, depth, 64, 64)
|
||||
}
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
import * as THREE from 'three'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { GUI } from 'lil-gui'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
import { getMyHand } from './hand'
|
||||
|
||||
export type HandItemBlock = {
|
||||
name
|
||||
properties
|
||||
name?
|
||||
properties?
|
||||
type: 'block' | 'item' | 'hand'
|
||||
id?: number
|
||||
}
|
||||
|
||||
export default class HoldingBlock {
|
||||
// TODO refactor with the tree builder for better visual understanding
|
||||
holdingBlock: THREE.Object3D | undefined = undefined
|
||||
swingAnimation: tweenJs.Group | undefined = undefined
|
||||
blockSwapAnimation: {
|
||||
|
|
@ -16,22 +21,25 @@ export default class HoldingBlock {
|
|||
hidden: boolean
|
||||
} | undefined = undefined
|
||||
cameraGroup = new THREE.Mesh()
|
||||
objectOuterGroup = new THREE.Group()
|
||||
objectInnerGroup = new THREE.Group()
|
||||
camera: THREE.Group | THREE.PerspectiveCamera
|
||||
objectOuterGroup = new THREE.Group() // 3
|
||||
objectInnerGroup = new THREE.Group() // 4
|
||||
holdingBlockInnerGroup = new THREE.Group() // 5
|
||||
camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100)
|
||||
stopUpdate = false
|
||||
lastHeldItem: HandItemBlock | undefined
|
||||
toBeRenderedItem: HandItemBlock | undefined
|
||||
isSwinging = false
|
||||
nextIterStopCallbacks: Array<() => void> | undefined
|
||||
rightSide = true
|
||||
|
||||
constructor (public scene: THREE.Scene) {
|
||||
debug = {} as Record<string, any>
|
||||
|
||||
constructor () {
|
||||
this.initCameraGroup()
|
||||
}
|
||||
|
||||
initCameraGroup () {
|
||||
this.cameraGroup = new THREE.Mesh()
|
||||
this.scene.add(this.cameraGroup)
|
||||
}
|
||||
|
||||
startSwing () {
|
||||
|
|
@ -44,17 +52,18 @@ export default class HoldingBlock {
|
|||
// const DURATION = 1000 * 0.35 / 2
|
||||
const DURATION = 1000 * 0.35 / 3
|
||||
// const DURATION = 1000
|
||||
const { position, rotation, object } = this.getFinalSwingPositionRotation()
|
||||
const initialPos = {
|
||||
x: this.objectInnerGroup.position.x,
|
||||
y: this.objectInnerGroup.position.y,
|
||||
z: this.objectInnerGroup.position.z
|
||||
x: object.position.x,
|
||||
y: object.position.y,
|
||||
z: object.position.z
|
||||
}
|
||||
const initialRot = {
|
||||
x: this.objectInnerGroup.rotation.x,
|
||||
y: this.objectInnerGroup.rotation.y,
|
||||
z: this.objectInnerGroup.rotation.z
|
||||
x: object.rotation.x,
|
||||
y: object.rotation.y,
|
||||
z: object.rotation.z
|
||||
}
|
||||
const mainAnim = new tweenJs.Tween(this.objectInnerGroup.position, this.swingAnimation).to({ y: this.objectInnerGroup.position.y - this.objectInnerGroup.scale.y / 2 }, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
const mainAnim = new tweenJs.Tween(object.position, this.swingAnimation).to(position, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
let i = 0
|
||||
mainAnim.onRepeat(() => {
|
||||
i++
|
||||
|
|
@ -67,14 +76,66 @@ export default class HoldingBlock {
|
|||
this.swingAnimation!.removeAll()
|
||||
this.swingAnimation = undefined
|
||||
// todo refactor to be more generic for animations
|
||||
this.objectInnerGroup.position.set(initialPos.x, initialPos.y, initialPos.z)
|
||||
// this.objectInnerGroup.rotation.set(initialRot.x, initialRot.y, initialRot.z)
|
||||
Object.assign(this.objectInnerGroup.rotation, initialRot)
|
||||
object.position.set(initialPos.x, initialPos.y, initialPos.z)
|
||||
// object.rotation.set(initialRot.x, initialRot.y, initialRot.z)
|
||||
Object.assign(object.rotation, initialRot)
|
||||
}
|
||||
})
|
||||
|
||||
new tweenJs.Tween(this.objectInnerGroup.rotation, this.swingAnimation).to({ z: THREE.MathUtils.degToRad(90) }, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
new tweenJs.Tween(this.objectInnerGroup.rotation, this.swingAnimation).to({ x: -THREE.MathUtils.degToRad(90) }, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
new tweenJs.Tween(object.rotation, this.swingAnimation).to(rotation, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
}
|
||||
}
|
||||
|
||||
getFinalSwingPositionRotation (origPosition?: THREE.Vector3) {
|
||||
const object = this.objectInnerGroup
|
||||
if (this.lastHeldItem?.type === 'block') {
|
||||
origPosition ??= object.position
|
||||
return {
|
||||
position: { y: origPosition.y - this.objectInnerGroup.scale.y / 2 },
|
||||
rotation: { z: THREE.MathUtils.degToRad(90), x: -THREE.MathUtils.degToRad(90) },
|
||||
object
|
||||
}
|
||||
}
|
||||
if (this.lastHeldItem?.type === 'item') {
|
||||
const object = this.holdingBlockInnerGroup
|
||||
origPosition ??= object.position
|
||||
return {
|
||||
position: {
|
||||
y: origPosition.y - object.scale.y * 2,
|
||||
// z: origPosition.z - window.zFinal,
|
||||
// x: origPosition.x - window.xFinal,
|
||||
},
|
||||
// rotation: { z: THREE.MathUtils.degToRad(90), x: -THREE.MathUtils.degToRad(90) }
|
||||
rotation: {
|
||||
// z: THREE.MathUtils.degToRad(window.zRotationFinal ?? 0),
|
||||
// x: THREE.MathUtils.degToRad(window.xRotationFinal ?? 0),
|
||||
// y: THREE.MathUtils.degToRad(window.yRotationFinal ?? 0),
|
||||
x: THREE.MathUtils.degToRad(-120)
|
||||
},
|
||||
object
|
||||
}
|
||||
}
|
||||
if (this.lastHeldItem?.type === 'hand') {
|
||||
const object = this.holdingBlockInnerGroup
|
||||
origPosition ??= object.position
|
||||
return {
|
||||
position: {
|
||||
y: origPosition.y - (window.yFinal ?? 0.15),
|
||||
z: origPosition.z - window.zFinal,
|
||||
x: origPosition.x - window.xFinal,
|
||||
},
|
||||
rotation: {
|
||||
x: THREE.MathUtils.degToRad(window.xRotationFinal || -14.7),
|
||||
y: THREE.MathUtils.degToRad(window.yRotationFinal || 33.95),
|
||||
z: THREE.MathUtils.degToRad(window.zRotationFinal || -28),
|
||||
},
|
||||
object
|
||||
}
|
||||
}
|
||||
return {
|
||||
position: {},
|
||||
rotation: {},
|
||||
object
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,11 +150,35 @@ export default class HoldingBlock {
|
|||
})
|
||||
}
|
||||
|
||||
update (camera: typeof this.camera) {
|
||||
this.camera = camera
|
||||
render (originalCamera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer, ambientLight: THREE.AmbientLight, directionalLight: THREE.DirectionalLight) {
|
||||
if (!this.lastHeldItem) return
|
||||
this.swingAnimation?.update()
|
||||
this.blockSwapAnimation?.tween.update()
|
||||
|
||||
const scene = new THREE.Scene()
|
||||
scene.add(this.cameraGroup)
|
||||
// if (this.camera.aspect !== originalCamera.aspect) {
|
||||
// this.camera.aspect = originalCamera.aspect
|
||||
// this.camera.updateProjectionMatrix()
|
||||
// }
|
||||
this.updateCameraGroup()
|
||||
scene.add(ambientLight.clone())
|
||||
scene.add(directionalLight.clone())
|
||||
|
||||
const viewerSize = renderer.getSize(new THREE.Vector2())
|
||||
const minSize = Math.min(viewerSize.width, viewerSize.height)
|
||||
|
||||
renderer.autoClear = false
|
||||
renderer.clearDepth()
|
||||
if (this.rightSide) {
|
||||
const x = viewerSize.width - minSize
|
||||
// if (x) x -= x / 4
|
||||
renderer.setViewport(x, 0, minSize, minSize)
|
||||
} else {
|
||||
renderer.setViewport(0, 0, minSize, minSize)
|
||||
}
|
||||
renderer.render(scene, this.camera)
|
||||
renderer.setViewport(0, 0, viewerSize.width, viewerSize.height)
|
||||
}
|
||||
|
||||
// worldTest () {
|
||||
|
|
@ -142,23 +227,36 @@ export default class HoldingBlock {
|
|||
this.cameraGroup.position.copy(camera.position)
|
||||
this.cameraGroup.rotation.copy(camera.rotation)
|
||||
|
||||
const viewerSize = viewer.renderer.getSize(new THREE.Vector2())
|
||||
// const x = window.x ?? 0.25 * viewerSize.width / viewerSize.height
|
||||
// const x = 0 * viewerSize.width / viewerSize.height
|
||||
const x = 0.2 * viewerSize.width / viewerSize.height
|
||||
this.objectOuterGroup.position.set(x, -0.3, -0.45)
|
||||
// const viewerSize = viewer.renderer.getSize(new THREE.Vector2())
|
||||
// const aspect = viewerSize.width / viewerSize.height
|
||||
const aspect = 1
|
||||
|
||||
|
||||
// Adjust the position based on the aspect ratio
|
||||
const { position, scale: scaleData } = this.getHandHeld3d()
|
||||
const distance = -position.z
|
||||
const side = this.rightSide ? 1 : -1
|
||||
this.objectOuterGroup.position.set(
|
||||
distance * position.x * aspect * side,
|
||||
distance * position.y,
|
||||
-distance
|
||||
)
|
||||
|
||||
// const scale = Math.min(0.8, Math.max(1, 1 * aspect))
|
||||
const scale = scaleData * 2.22 * 0.2
|
||||
this.objectOuterGroup.scale.set(scale, scale, scale)
|
||||
}
|
||||
|
||||
async initHandObject (material: THREE.Material, blockstatesModels: any, blocksAtlases: any, block?: HandItemBlock) {
|
||||
async initHandObject (material: THREE.Material, blockstatesModels: any, blocksAtlases: any, handItem?: HandItemBlock) {
|
||||
let animatingCurrent = false
|
||||
if (!this.swingAnimation && !this.blockSwapAnimation && this.isDifferentItem(block)) {
|
||||
if (!this.swingAnimation && !this.blockSwapAnimation && this.isDifferentItem(handItem)) {
|
||||
animatingCurrent = true
|
||||
await this.playBlockSwapAnimation()
|
||||
this.holdingBlock?.removeFromParent()
|
||||
this.holdingBlock = undefined
|
||||
}
|
||||
this.lastHeldItem = block
|
||||
if (!block) {
|
||||
this.lastHeldItem = handItem
|
||||
if (!handItem) {
|
||||
this.holdingBlock?.removeFromParent()
|
||||
this.holdingBlock = undefined
|
||||
this.swingAnimation = undefined
|
||||
|
|
@ -166,16 +264,28 @@ export default class HoldingBlock {
|
|||
return
|
||||
}
|
||||
const blockProvider = worldBlockProvider(blockstatesModels, blocksAtlases, 'latest')
|
||||
const models = blockProvider.getAllResolvedModels0_1(block, true)
|
||||
const blockInner = getThreeBlockModelGroup(material, models, undefined, 'plains', loadedData)
|
||||
// const { mesh: itemMesh } = viewer.entities.getItemMesh({
|
||||
// itemId: 541,
|
||||
// })!
|
||||
// itemMesh.position.set(0.5, 0.5, 0.5)
|
||||
// const blockInner = itemMesh
|
||||
let blockInner
|
||||
if (handItem.type === 'block') {
|
||||
const models = blockProvider.getAllResolvedModels0_1({
|
||||
name: handItem.name,
|
||||
properties: handItem.properties ?? {}
|
||||
}, true)
|
||||
blockInner = getThreeBlockModelGroup(material, models, undefined, 'plains', loadedData)
|
||||
} else if (handItem.type === 'item') {
|
||||
const { mesh: itemMesh } = viewer.entities.getItemMesh({
|
||||
itemId: handItem.id,
|
||||
})!
|
||||
itemMesh.position.set(0.5, 0.5, 0.5)
|
||||
blockInner = itemMesh
|
||||
} else {
|
||||
blockInner = await getMyHand()
|
||||
}
|
||||
blockInner.name = 'holdingBlock'
|
||||
const blockOuterGroup = new THREE.Group()
|
||||
blockOuterGroup.add(blockInner)
|
||||
this.holdingBlockInnerGroup.removeFromParent()
|
||||
this.holdingBlockInnerGroup = new THREE.Group()
|
||||
this.holdingBlockInnerGroup.add(blockInner)
|
||||
blockOuterGroup.add(this.holdingBlockInnerGroup)
|
||||
this.holdingBlock = blockInner
|
||||
this.objectInnerGroup = new THREE.Group()
|
||||
this.objectInnerGroup.add(blockOuterGroup)
|
||||
|
|
@ -190,18 +300,113 @@ export default class HoldingBlock {
|
|||
this.objectOuterGroup.add(this.objectInnerGroup)
|
||||
|
||||
this.cameraGroup.add(this.objectOuterGroup)
|
||||
const rotation = -45 + -90
|
||||
// const rotation = -45 // should be for item
|
||||
this.holdingBlock.rotation.set(0, THREE.MathUtils.degToRad(rotation), 0, 'ZYX')
|
||||
const rotationDeg = this.getHandHeld3d().rotation
|
||||
let origPosition
|
||||
const setRotation = () => {
|
||||
const final = this.getFinalSwingPositionRotation(origPosition)
|
||||
origPosition ??= final.object.position.clone()
|
||||
if (this.debug.displayFinal) {
|
||||
Object.assign(final.object.position, final.position)
|
||||
Object.assign(final.object.rotation, final.rotation)
|
||||
} else if (this.debug.displayFinal === false) {
|
||||
final.object.rotation.set(0, 0, 0)
|
||||
}
|
||||
|
||||
// const scale = window.scale ?? 0.2
|
||||
const scale = 0.2
|
||||
this.objectOuterGroup.scale.set(scale, scale, scale)
|
||||
// this.objectOuterGroup.position.set(x, window.y ?? -0.41, window.z ?? -0.45)
|
||||
// this.objectOuterGroup.position.set(x, 0, -0.45)
|
||||
this.holdingBlock!.rotation.x = THREE.MathUtils.degToRad(rotationDeg.x)
|
||||
this.holdingBlock!.rotation.y = THREE.MathUtils.degToRad(rotationDeg.y)
|
||||
this.holdingBlock!.rotation.z = THREE.MathUtils.degToRad(rotationDeg.z)
|
||||
this.objectOuterGroup.rotation.y = THREE.MathUtils.degToRad(rotationDeg.yOuter)
|
||||
}
|
||||
// const gui = new GUI()
|
||||
// gui.add(rotationDeg, 'x', -180, 180, 0.1)
|
||||
// gui.add(rotationDeg, 'y', -180, 180, 0.1)
|
||||
// gui.add(rotationDeg, 'z', -180, 180, 0.1)
|
||||
// gui.add(rotationDeg, 'yOuter', -180, 180, 0.1)
|
||||
// Object.assign(window, { xFinal: 0, yFinal: 0, zFinal: 0, xRotationFinal: 0, yRotationFinal: 0, zRotationFinal: 0, displayFinal: true })
|
||||
// gui.add(window, 'xFinal', -10, 10, 0.05)
|
||||
// gui.add(window, 'yFinal', -10, 10, 0.05)
|
||||
// gui.add(window, 'zFinal', -10, 10, 0.05)
|
||||
// gui.add(window, 'xRotationFinal', -180, 180, 0.05)
|
||||
// gui.add(window, 'yRotationFinal', -180, 180, 0.05)
|
||||
// gui.add(window, 'zRotationFinal', -180, 180, 0.05)
|
||||
// gui.add(window, 'displayFinal')
|
||||
// gui.onChange(setRotation)
|
||||
setRotation()
|
||||
|
||||
if (animatingCurrent) {
|
||||
await this.playBlockSwapAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
getHandHeld3d () {
|
||||
const type = this.lastHeldItem?.type ?? 'hand'
|
||||
const { debug } = this
|
||||
|
||||
let scale = type === 'item' ? 0.68 : 0.45
|
||||
|
||||
const position = {
|
||||
x: debug.x ?? 0.4,
|
||||
y: debug.y ?? -0.7,
|
||||
z: -0.45
|
||||
}
|
||||
|
||||
if (type === 'item') {
|
||||
position.x = -0.05
|
||||
// position.y -= 3.2 / 10
|
||||
// position.z += 1.13 / 10
|
||||
}
|
||||
|
||||
if (type === 'hand') {
|
||||
// position.x = viewer.camera.aspect > 1 ? 0.7 : 1.1
|
||||
position.y = -0.8
|
||||
scale = 0.8
|
||||
}
|
||||
|
||||
const rotations = {
|
||||
block: {
|
||||
x: 0,
|
||||
y: -45 + 90,
|
||||
z: 0,
|
||||
yOuter: 0
|
||||
},
|
||||
// hand: {
|
||||
// x: 166.7,
|
||||
// // y: -180,
|
||||
// y: -165.2,
|
||||
// // z: -156.3,
|
||||
// z: -134.2,
|
||||
// yOuter: -81.1
|
||||
// },
|
||||
hand: {
|
||||
x: -32.4,
|
||||
// y: 25.1
|
||||
y: 42.8,
|
||||
z: -41.3,
|
||||
yOuter: 0
|
||||
},
|
||||
// item: {
|
||||
// x: -174,
|
||||
// y: 47.3,
|
||||
// z: -134.2,
|
||||
// yOuter: -41.2
|
||||
// }
|
||||
item: {
|
||||
// x: -174,
|
||||
// y: 47.3,
|
||||
// z: -134.2,
|
||||
// yOuter: -41.2
|
||||
x: 0,
|
||||
// y: -90, // todo thats the correct one but we don't make it look too cheap because of no depth
|
||||
y: -70,
|
||||
z: window.z ?? 25,
|
||||
yOuter: 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rotation: rotations[type],
|
||||
position,
|
||||
scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ setInterval(() => {
|
|||
const geometry = getSectionGeometry(x, y, z, world)
|
||||
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean)
|
||||
//@ts-expect-error
|
||||
postMessage({ type: 'geometry', key, geometry }, transferable)
|
||||
postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable)
|
||||
processTime = performance.now() - start
|
||||
} else {
|
||||
// console.info('[mesher] Missing section', x, y, z)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import legacyJson from '../../../../src/preflatMap.json'
|
|||
import { BlockType } from '../../../examples/shared'
|
||||
import { World, BlockModelPartsResolved, WorldBlock as Block } from './world'
|
||||
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
|
||||
import { MesherGeometryOutput } from './shared'
|
||||
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||
import { MesherGeometryOutput, HighestBlockInfo } from './shared'
|
||||
|
||||
|
||||
let blockProvider: WorldBlockProvider
|
||||
|
||||
|
|
@ -226,14 +228,12 @@ const identicalCull = (currentElement: BlockElement, neighbor: Block, direction:
|
|||
const models = neighbor.models?.map(m => m[useVar] ?? m[0]) ?? []
|
||||
// TODO we should support it! rewrite with optimizing general pipeline
|
||||
if (models.some(m => m.x || m.y || m.z)) return
|
||||
for (const model of models) {
|
||||
for (const element of model.elements ?? []) {
|
||||
return models.every(model => {
|
||||
return (model.elements ?? []).every(element => {
|
||||
// todo check alfa on texture
|
||||
if (element.faces[lookForOppositeSide]?.cullface && elemCompareForm(currentElement) === elemCompareForm(element) && elementEdgeValidator(element)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return !!(element.faces[lookForOppositeSide]?.cullface && elemCompareForm(currentElement) === elemCompareForm(element) && elementEdgeValidator(element))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let needSectionRecomputeOnChange = false
|
||||
|
|
@ -439,8 +439,6 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
}
|
||||
}
|
||||
|
||||
const invisibleBlocks = new Set(['air', 'cave_air', 'void_air', 'barrier'])
|
||||
|
||||
const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true'
|
||||
|
||||
let unknownBlockModel: BlockModelPartsResolved
|
||||
|
|
@ -464,7 +462,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
// todo this can be removed here
|
||||
signs: {},
|
||||
// isFull: true,
|
||||
highestBlocks: {}, // todo migrate to map for 2% boost perf
|
||||
highestBlocks: new Map<string, HighestBlockInfo>([]),
|
||||
hadErrors: false,
|
||||
blocksCount: 0
|
||||
}
|
||||
|
|
@ -474,16 +472,13 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
||||
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
||||
let block = world.getBlock(cursor, blockProvider, attr)!
|
||||
if (!invisibleBlocks.has(block.name)) {
|
||||
const highest = attr.highestBlocks[`${cursor.x},${cursor.z}`]
|
||||
if (!INVISIBLE_BLOCKS.has(block.name)) {
|
||||
const highest = attr.highestBlocks.get(`${cursor.x},${cursor.z}`)
|
||||
if (!highest || highest.y < cursor.y) {
|
||||
attr.highestBlocks[`${cursor.x},${cursor.z}`] = {
|
||||
y: cursor.y,
|
||||
name: block.name
|
||||
}
|
||||
attr.highestBlocks.set(`${cursor.x},${cursor.z}`, { y: cursor.y, stateId: block.stateId, biomeId: block.biome.id })
|
||||
}
|
||||
}
|
||||
if (invisibleBlocks.has(block.name)) continue
|
||||
if (INVISIBLE_BLOCKS.has(block.name)) continue
|
||||
if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) {
|
||||
const key = `${cursor.x},${cursor.y},${cursor.z}`
|
||||
const props: any = block.getProperties()
|
||||
|
|
@ -531,7 +526,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr)
|
||||
attr.blocksCount++
|
||||
}
|
||||
if (block.name !== 'water' && block.name !== 'lava' && !invisibleBlocks.has(block.name)) {
|
||||
if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
|
||||
// cache
|
||||
let { models } = block
|
||||
|
||||
|
|
@ -624,8 +619,8 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
return attr
|
||||
}
|
||||
|
||||
export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTiles = false, useUnknownBlockModel = true) => {
|
||||
blockProvider = worldBlockProvider(blockstatesModels, blocksAtlas, 'latest')
|
||||
export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTiles = false, useUnknownBlockModel = true, version = 'latest') => {
|
||||
blockProvider = worldBlockProvider(blockstatesModels, blocksAtlas, version)
|
||||
globalThis.blockProvider = blockProvider
|
||||
if (useUnknownBlockModel) {
|
||||
unknownBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'unknown', properties: {} })
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ export type MesherGeometryOutput = {
|
|||
tiles: Record<string, BlockType>,
|
||||
signs: Record<string, any>,
|
||||
// isFull: boolean
|
||||
highestBlocks: Record<string, { y: number, name: string }>
|
||||
highestBlocks: Map<string, HighestBlockInfo>
|
||||
hadErrors: boolean
|
||||
blocksCount: number
|
||||
}
|
||||
|
||||
export type HighestBlockInfo = { y: number, stateId: number | undefined, biomeId: number | undefined }
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const setup = (version, initialBlocks: Array<[number[], string]>) => {
|
|||
}
|
||||
}
|
||||
|
||||
setBlockStatesData(blockStatesModels, blocksAtlasesJson, true, false)
|
||||
setBlockStatesData(blockStatesModels, blocksAtlasesJson, true, false, version)
|
||||
const reload = () => {
|
||||
mesherWorld.removeColumn(0, 0)
|
||||
mesherWorld.addColumn(0, 0, chunk1.toJson())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import supportedVersions from '../../../../../src/supportedVersions.mjs'
|
||||
import { INVISIBLE_BLOCKS } from '../worldConstants'
|
||||
import { setup } from './mesherTester'
|
||||
|
||||
const lastVersion = supportedVersions.at(-1)
|
||||
|
|
@ -16,7 +17,7 @@ const addPositions = [
|
|||
|
||||
test('Known blocks are not rendered', () => {
|
||||
const { mesherWorld, getGeometry, pos, mcData } = setup(lastVersion, addPositions as any)
|
||||
const ignoreAsExpected = new Set(['air', 'cave_air', 'void_air', 'barrier', 'water', 'lava', 'moving_piston', 'light'])
|
||||
const ignoreAsExpected = new Set([...INVISIBLE_BLOCKS, 'water', 'lava', 'moving_piston', 'light'])
|
||||
|
||||
let time = 0
|
||||
let times = 0
|
||||
|
|
@ -42,11 +43,14 @@ test('Known blocks are not rendered', () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
console.log('Checking blocks of version', lastVersion)
|
||||
console.log('Average time', time / times)
|
||||
// should be fixed, but to avoid regressions & for visibility
|
||||
// TODO resolve creaking_heart issue (1.21.3)
|
||||
expect(missingBlocks).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bubble_column": true,
|
||||
"creaking_heart": true,
|
||||
"end_gateway": true,
|
||||
"end_portal": true,
|
||||
"structure_void": true,
|
||||
|
|
|
|||
1
prismarine-viewer/viewer/lib/mesher/worldConstants.ts
Normal file
1
prismarine-viewer/viewer/lib/mesher/worldConstants.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const INVISIBLE_BLOCKS = new Set(['air', 'void_air', 'cave_air', 'barrier'])
|
||||
|
|
@ -3,12 +3,14 @@ import * as THREE from 'three'
|
|||
import { Vec3 } from 'vec3'
|
||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
import { Entities } from './entities'
|
||||
import { Primitives } from './primitives'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
import { addNewStat } from './ui/newStats'
|
||||
import { getMyHand } from './hand'
|
||||
|
||||
export class Viewer {
|
||||
scene: THREE.Scene
|
||||
|
|
@ -26,6 +28,8 @@ export class Viewer {
|
|||
renderingUntilNoUpdates = false
|
||||
processEntityOverrides = (e, overrides) => overrides
|
||||
|
||||
getMineflayerBot (): void | Record<string, any> {} // to be overridden
|
||||
|
||||
get camera () {
|
||||
return this.world.camera
|
||||
}
|
||||
|
|
@ -78,15 +82,16 @@ export class Viewer {
|
|||
// this.primitives.clear()
|
||||
}
|
||||
|
||||
setVersion (userVersion: string, texturesVersion = userVersion) {
|
||||
setVersion (userVersion: string, texturesVersion = userVersion): void | Promise<void> {
|
||||
console.log('[viewer] Using version:', userVersion, 'textures:', texturesVersion)
|
||||
void this.world.setVersion(userVersion, texturesVersion).then(async () => {
|
||||
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')
|
||||
})
|
||||
this.entities.clear()
|
||||
// this.primitives.clear()
|
||||
}
|
||||
|
||||
addColumn (x, z, chunk, isLightUpdate = false) {
|
||||
|
|
@ -98,24 +103,26 @@ export class Viewer {
|
|||
}
|
||||
|
||||
setBlockStateId (pos: Vec3, stateId: number) {
|
||||
if (!this.world.loadedChunks[`${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`]) {
|
||||
console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
|
||||
const set = async () => {
|
||||
const sectionX = Math.floor(pos.x / 16) * 16
|
||||
const sectionZ = Math.floor(pos.z / 16) * 16
|
||||
if (this.world.queuedChunks.has(`${sectionX},${sectionZ}`)) {
|
||||
await this.world.waitForChunkToLoad(pos)
|
||||
}
|
||||
if (!this.world.loadedChunks[`${sectionX},${sectionZ}`]) {
|
||||
console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
|
||||
}
|
||||
this.world.setBlockStateId(pos, stateId)
|
||||
}
|
||||
this.world.setBlockStateId(pos, stateId)
|
||||
void set()
|
||||
}
|
||||
|
||||
demoModel () {
|
||||
async demoModel () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const blockProvider = worldBlockProvider(this.world.blockstatesModels, this.world.blocksAtlases, 'latest')
|
||||
const models = blockProvider.getAllResolvedModels0_1({
|
||||
name: 'item_frame',
|
||||
properties: {
|
||||
// map: false
|
||||
}
|
||||
}, true)
|
||||
const { material } = this.world
|
||||
const mesh = getThreeBlockModelGroup(material, models, undefined, 'plains', loadedData)
|
||||
|
||||
const mesh = await getMyHand()
|
||||
// mesh.rotation.y = THREE.MathUtils.degToRad(90)
|
||||
setBlockPosition(mesh, pos)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
|
|
@ -150,12 +157,11 @@ export class Viewer {
|
|||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
let yOffset = this.playerHeight
|
||||
let yOffset = this.getMineflayerBot()?.entity?.eyeHeight ?? this.playerHeight
|
||||
if (this.isSneaking) yOffset -= 0.3
|
||||
|
||||
if (this.world instanceof WorldRendererThree) {
|
||||
this.world.camera = cam as THREE.PerspectiveCamera
|
||||
}
|
||||
this.world.camera = cam as THREE.PerspectiveCamera
|
||||
|
||||
this.world.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +211,7 @@ export class Viewer {
|
|||
} | null
|
||||
worldEmitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => {
|
||||
this.world.worldConfig = worldConfig
|
||||
this.world.queuedChunks.add(`${x},${z}`)
|
||||
const args = [x, z, chunk, isLightUpdate]
|
||||
if (!currentLoadChunkBatch) {
|
||||
// add a setting to use debounce instead
|
||||
|
|
@ -212,6 +219,7 @@ export class Viewer {
|
|||
data: [],
|
||||
timeout: setTimeout(() => {
|
||||
for (const args of currentLoadChunkBatch!.data) {
|
||||
this.world.queuedChunks.delete(`${args[0]},${args[1]}`)
|
||||
this.addColumn(...args as Parameters<typeof this.addColumn>)
|
||||
}
|
||||
currentLoadChunkBatch = null
|
||||
|
|
@ -222,7 +230,7 @@ export class Viewer {
|
|||
})
|
||||
// todo remove and use other architecture instead so data flow is clear
|
||||
worldEmitter.on('blockEntities', (blockEntities) => {
|
||||
if (this.world instanceof WorldRendererThree) this.world.blockEntities = blockEntities
|
||||
if (this.world instanceof WorldRendererThree) (this.world).blockEntities = blockEntities
|
||||
})
|
||||
|
||||
worldEmitter.on('unloadChunk', ({ x, z }) => {
|
||||
|
|
@ -237,14 +245,24 @@ export class Viewer {
|
|||
this.world.updateViewerPosition(pos)
|
||||
})
|
||||
|
||||
|
||||
worldEmitter.on('renderDistance', (d) => {
|
||||
this.world.viewDistance = d
|
||||
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
})
|
||||
|
||||
worldEmitter.on('renderDistance', (d) => {
|
||||
this.world.viewDistance = d
|
||||
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
this.world.allChunksFinished = Object.keys(this.world.finishedChunks).length === this.world.chunksLength
|
||||
})
|
||||
|
||||
worldEmitter.on('markAsLoaded', ({ x, z }) => {
|
||||
this.world.markAsLoaded(x, z)
|
||||
})
|
||||
|
||||
worldEmitter.on('updateLight', ({ pos }) => {
|
||||
if (this.world instanceof WorldRendererThree) this.world.updateLight(pos.x, pos.z)
|
||||
if (this.world instanceof WorldRendererThree) (this.world).updateLight(pos.x, pos.z)
|
||||
})
|
||||
|
||||
worldEmitter.on('time', (timeOfDay) => {
|
||||
|
|
@ -264,16 +282,20 @@ export class Viewer {
|
|||
skyLight = Math.floor(skyLight) // todo: remove this after optimization
|
||||
|
||||
if (this.world.mesherConfig.skyLight === skyLight) return
|
||||
this.world.mesherConfig.skyLight = skyLight;
|
||||
(this.world as WorldRendererThree).rerenderAllChunks?.()
|
||||
this.world.mesherConfig.skyLight = skyLight
|
||||
if (this.world instanceof WorldRendererThree) {
|
||||
(this.world).rerenderAllChunks?.()
|
||||
}
|
||||
})
|
||||
|
||||
worldEmitter.emit('listening')
|
||||
}
|
||||
|
||||
render () {
|
||||
this.world.render()
|
||||
this.entities.render()
|
||||
if (this.world instanceof WorldRendererThree) {
|
||||
(this.world).render()
|
||||
this.entities.render()
|
||||
}
|
||||
}
|
||||
|
||||
async waitForChunksToRender () {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { generateSpiralMatrix, ViewRect } from 'flying-squid/dist/utils'
|
|||
import { Vec3 } from 'vec3'
|
||||
import { BotEvents } from 'mineflayer'
|
||||
import { getItemFromBlock } from '../../../src/chatUtils'
|
||||
import { delayedIterator } from '../../examples/shared'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
|
||||
export type ChunkPosKey = string
|
||||
|
|
@ -22,14 +23,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
private readonly emitter: WorldDataEmitter
|
||||
keepChunksDistance = 0
|
||||
addWaitTime = 1
|
||||
_handDisplay = false
|
||||
get handDisplay () {
|
||||
return this._handDisplay
|
||||
}
|
||||
set handDisplay (newVal) {
|
||||
this._handDisplay = newVal
|
||||
this.eventListeners.heldItemChanged?.()
|
||||
}
|
||||
isPlayground = false
|
||||
|
||||
constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
|
||||
super()
|
||||
|
|
@ -105,23 +99,29 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
time: () => {
|
||||
this.emitter.emit('time', bot.time.timeOfDay)
|
||||
},
|
||||
heldItemChanged: () => {
|
||||
if (!this.handDisplay) {
|
||||
viewer.world.onHandItemSwitch(undefined)
|
||||
return
|
||||
}
|
||||
const newItem = bot.heldItem
|
||||
if (!newItem) {
|
||||
viewer.world.onHandItemSwitch(undefined)
|
||||
return
|
||||
}
|
||||
const block = loadedData.blocksByName[newItem.name]
|
||||
// todo clean types
|
||||
const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {}
|
||||
viewer.world.onHandItemSwitch({ name: newItem.name, properties: blockProperties })
|
||||
heldItemChanged () {
|
||||
handChanged(false)
|
||||
},
|
||||
} satisfies Partial<BotEvents>
|
||||
this.eventListeners.heldItemChanged()
|
||||
const handChanged = (isLeftHand: boolean) => {
|
||||
const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem
|
||||
if (!newItem) {
|
||||
viewer.world.onHandItemSwitch(undefined, isLeftHand)
|
||||
return
|
||||
}
|
||||
const block = loadedData.blocksByName[newItem.name]
|
||||
// todo clean types
|
||||
const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {}
|
||||
// todo item props
|
||||
viewer.world.onHandItemSwitch({ name: newItem.name, properties: blockProperties, id: newItem.type, type: block ? 'block' : 'item', }, isLeftHand)
|
||||
}
|
||||
bot.inventory.on('updateSlot', (index) => {
|
||||
if (index === 45) {
|
||||
handChanged(true)
|
||||
}
|
||||
})
|
||||
handChanged(false)
|
||||
handChanged(true)
|
||||
|
||||
|
||||
bot._client.on('update_light', ({ chunkX, chunkZ }) => {
|
||||
|
|
@ -173,19 +173,11 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
}
|
||||
|
||||
async _loadChunks (positions: Vec3[], sliceSize = 5) {
|
||||
let i = 0
|
||||
const promises = [] as Array<Promise<void>>
|
||||
return new Promise<void>(resolve => {
|
||||
const interval = setInterval(() => {
|
||||
if (i >= positions.length) {
|
||||
clearInterval(interval)
|
||||
void Promise.all(promises).then(() => resolve())
|
||||
return
|
||||
}
|
||||
promises.push(this.loadChunk(positions[i]))
|
||||
i++
|
||||
}, this.addWaitTime)
|
||||
await delayedIterator(positions, this.addWaitTime, (pos) => {
|
||||
promises.push(this.loadChunk(pos))
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
readdDebug () {
|
||||
|
|
@ -221,6 +213,8 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
//@ts-expect-error
|
||||
this.emitter.emit('loadChunk', { x: pos.x, z: pos.z, chunk, blockEntities: column.blockEntities, worldConfig, isLightUpdate })
|
||||
this.loadedChunks[`${pos.x},${pos.z}`] = true
|
||||
} else if (this.isPlayground) { // don't allow in real worlds pre-flag chunks as loaded to avoid race condition when the chunk might still be loading. In playground it's assumed we always pre-load all chunks first
|
||||
this.emitter.emit('markAsLoaded', { x: pos.x, z: pos.z })
|
||||
}
|
||||
} else {
|
||||
// console.debug('skipped loading chunk', dx, dz, '>', this.viewDistance)
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
|
|||
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
|
||||
import { AtlasParser } from 'mc-assets'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { LineMaterial } from 'three-stdlib'
|
||||
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
||||
import { toMajorVersion } from '../../../src/utils'
|
||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||
import { MesherGeometryOutput, defaultMesherConfig } from './mesher/shared'
|
||||
import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput } from './mesher/shared'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
import { HandItemBlock } from './holdingBlock'
|
||||
import { updateStatText } from './ui/newStats'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
|
|
@ -27,7 +29,9 @@ export const worldCleanup = buildCleanupDecorator('resetWorld')
|
|||
|
||||
export const defaultWorldRendererConfig = {
|
||||
showChunkBorders: false,
|
||||
numWorkers: 4
|
||||
numWorkers: 4,
|
||||
// game renderer setting actually
|
||||
displayHand: false
|
||||
}
|
||||
|
||||
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
||||
|
|
@ -38,8 +42,14 @@ type CustomTexturesData = {
|
|||
}
|
||||
|
||||
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
|
||||
// todo
|
||||
@worldCleanup()
|
||||
threejsCursorLineMaterial: LineMaterial
|
||||
@worldCleanup()
|
||||
cursorBlock = null as Vec3 | null
|
||||
isPlayground = false
|
||||
displayStats = true
|
||||
@worldCleanup()
|
||||
worldConfig = { minY: 0, worldHeight: 256 }
|
||||
// todo need to cleanup
|
||||
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
|
||||
|
|
@ -49,27 +59,37 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
version = undefined as string | undefined
|
||||
@worldCleanup()
|
||||
loadedChunks = {} as Record<string, boolean>
|
||||
loadedChunks = {} as Record<string, boolean> // data is added for these chunks and they might be still processing
|
||||
|
||||
@worldCleanup()
|
||||
finishedChunks = {} as Record<string, boolean>
|
||||
finishedChunks = {} as Record<string, boolean> // these chunks are fully loaded into the world (scene)
|
||||
|
||||
@worldCleanup()
|
||||
sectionsOutstanding = new Map<string, number>()
|
||||
// loading sections (chunks)
|
||||
sectionsWaiting = new Map<string, number>()
|
||||
|
||||
@worldCleanup()
|
||||
queuedChunks = new Set<string>()
|
||||
|
||||
@worldCleanup()
|
||||
renderUpdateEmitter = new EventEmitter() as unknown as TypedEmitter<{
|
||||
dirty (pos: Vec3, value: boolean): void
|
||||
update (/* pos: Vec3, value: boolean */): void
|
||||
textureDownloaded (): void
|
||||
itemsTextureDownloaded (): void
|
||||
chunkFinished (key: string): void
|
||||
}>
|
||||
customTexturesDataUrl = undefined as string | undefined
|
||||
@worldCleanup()
|
||||
currentTextureImage = undefined as any
|
||||
workers: any[] = []
|
||||
@worldCleanup()
|
||||
viewerPosition?: Vec3
|
||||
lastCamUpdate = 0
|
||||
droppedFpsPercentage = 0
|
||||
@worldCleanup()
|
||||
initialChunkLoadWasStartedIn: number | undefined
|
||||
@worldCleanup()
|
||||
initialChunksLoad = true
|
||||
enableChunksLoadDelay = false
|
||||
texturesVersion?: string
|
||||
|
|
@ -81,7 +101,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
handleResize = () => { }
|
||||
mesherConfig = defaultMesherConfig
|
||||
camera: THREE.PerspectiveCamera
|
||||
highestBlocks: Record<string, { y: number, name: string }> = {}
|
||||
highestBlocks = new Map<string, HighestBlockInfo>()
|
||||
blockstatesModels: any
|
||||
customBlockStates: Record<string, any> | undefined
|
||||
customModels: Record<string, any> | undefined
|
||||
|
|
@ -97,6 +117,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
workersProcessAverageTime = 0
|
||||
workersProcessAverageTimeCount = 0
|
||||
maxWorkersProcessTime = 0
|
||||
geometryReceiveCount = {}
|
||||
allLoadedIn: undefined | number
|
||||
rendererDevice = '...'
|
||||
|
||||
edgeChunks = {} as Record<string, boolean>
|
||||
lastAddChunk = null as null | {
|
||||
timeout: any
|
||||
|
|
@ -108,13 +132,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
abstract outputFormat: 'threeJs' | 'webgpu'
|
||||
|
||||
abstract changeBackgroundColor (color: [number, number, number]): void
|
||||
|
||||
constructor (public config: WorldRendererConfig) {
|
||||
// this.initWorkers(1) // preload script on page load
|
||||
this.snapshotInitialValues()
|
||||
|
||||
this.renderUpdateEmitter.on('update', () => {
|
||||
const loadedChunks = Object.keys(this.finishedChunks).length
|
||||
updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance})`)
|
||||
updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance}/${this.viewDistance})`)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +148,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
initWorkers (numWorkers = this.config.numWorkers) {
|
||||
// init workers
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
for (let i = 0; i < numWorkers + 1; i++) {
|
||||
// Node environment needs an absolute path, but browser needs the url of the file
|
||||
const workerName = 'mesher.js'
|
||||
// eslint-disable-next-line node/no-path-concat
|
||||
|
|
@ -133,6 +159,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
if (!this.active) return
|
||||
this.handleWorkerMessage(data)
|
||||
if (data.type === 'geometry') {
|
||||
this.geometryReceiveCount[data.workerIndex] ??= 0
|
||||
this.geometryReceiveCount[data.workerIndex]++
|
||||
const geometry = data.geometry as MesherGeometryOutput
|
||||
for (const key in geometry.highestBlocks) {
|
||||
const highest = geometry.highestBlocks[key]
|
||||
|
|
@ -144,13 +172,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2])))
|
||||
}
|
||||
if (data.type === 'sectionFinished') { // on after load & unload section
|
||||
if (!this.sectionsOutstanding.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`)
|
||||
this.sectionsOutstanding.set(data.key, this.sectionsOutstanding.get(data.key)! - 1)
|
||||
if (this.sectionsOutstanding.get(data.key) === 0) this.sectionsOutstanding.delete(data.key)
|
||||
if (!this.sectionsWaiting.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`)
|
||||
this.sectionsWaiting.set(data.key, this.sectionsWaiting.get(data.key)! - 1)
|
||||
if (this.sectionsWaiting.get(data.key) === 0) this.sectionsWaiting.delete(data.key)
|
||||
|
||||
const chunkCoords = data.key.split(',').map(Number)
|
||||
if (this.loadedChunks[`${chunkCoords[0]},${chunkCoords[2]}`]) { // ensure chunk data was added, not a neighbor chunk update
|
||||
const loadingKeys = [...this.sectionsOutstanding.keys()]
|
||||
const loadingKeys = [...this.sectionsWaiting.keys()]
|
||||
if (!loadingKeys.some(key => {
|
||||
const [x, y, z] = key.split(',').map(Number)
|
||||
return x === chunkCoords[0] && z === chunkCoords[2]
|
||||
|
|
@ -158,13 +186,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.finishedChunks[`${chunkCoords[0]},${chunkCoords[2]}`] = true
|
||||
}
|
||||
}
|
||||
if (this.sectionsOutstanding.size === 0) {
|
||||
const allFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
||||
if (allFinished) {
|
||||
this.allChunksLoaded?.()
|
||||
this.allChunksFinished = true
|
||||
}
|
||||
}
|
||||
this.checkAllFinished()
|
||||
|
||||
this.renderUpdateEmitter.emit('update')
|
||||
if (data.processTime) {
|
||||
|
|
@ -187,8 +209,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined): void { }
|
||||
changeHandSwingingState (isAnimationPlaying: boolean): void { }
|
||||
checkAllFinished () {
|
||||
if (this.sectionsWaiting.size === 0) {
|
||||
const allFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
||||
if (allFinished) {
|
||||
this.allChunksLoaded?.()
|
||||
this.allChunksFinished = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined, isLeftHand: boolean): void { }
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
|
||||
|
||||
abstract handleWorkerMessage (data: WorkerReceive): void
|
||||
|
||||
|
|
@ -268,7 +300,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
}
|
||||
|
||||
async updateTexturesData () {
|
||||
async updateTexturesData (resourcePackUpdate = false) {
|
||||
const blocksAssetsParser = new AtlasParser(this.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
|
||||
const itemsAssetsParser = new AtlasParser(this.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
|
||||
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
|
||||
|
|
@ -325,11 +357,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
return Math.floor(Math.max(this.worldConfig.minY, this.mesherConfig.clipWorldBelowY ?? -Infinity) / 16) * 16
|
||||
}
|
||||
|
||||
updateChunksStatsText () {
|
||||
updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`)
|
||||
}
|
||||
|
||||
addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) {
|
||||
if (!this.active) return
|
||||
if (this.workers.length === 0) throw new Error('workers not initialized yet')
|
||||
this.initialChunksLoad = false
|
||||
this.initialChunkLoadWasStartedIn ??= Date.now()
|
||||
this.loadedChunks[`${x},${z}`] = true
|
||||
this.updateChunksStatsText()
|
||||
for (const worker of this.workers) {
|
||||
// todo optimize
|
||||
worker.postMessage({ type: 'chunk', x, z, chunk })
|
||||
|
|
@ -346,13 +384,19 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
}
|
||||
|
||||
markAsLoaded (x, z) {
|
||||
this.loadedChunks[`${x},${z}`] = true
|
||||
this.finishedChunks[`${x},${z}`] = true
|
||||
this.checkAllFinished()
|
||||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
delete this.loadedChunks[`${x},${z}`]
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'unloadChunk', x, z })
|
||||
}
|
||||
this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
||||
delete this.finishedChunks[`${x},${z}`]
|
||||
this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
}
|
||||
|
|
@ -369,24 +413,31 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
|
||||
setBlockStateId (pos: Vec3, stateId: number) {
|
||||
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
||||
const useChangeWorker = !this.sectionsWaiting[key]
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'blockUpdate', pos, stateId })
|
||||
}
|
||||
this.setSectionDirty(pos)
|
||||
this.setSectionDirty(pos, true, useChangeWorker)
|
||||
if (this.neighborChunkUpdates) {
|
||||
if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0))
|
||||
if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0))
|
||||
if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0))
|
||||
if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0))
|
||||
if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16))
|
||||
if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16))
|
||||
if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0), true, useChangeWorker)
|
||||
if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0), true, useChangeWorker)
|
||||
if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0), true, useChangeWorker)
|
||||
if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0), true, useChangeWorker)
|
||||
if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16), true, useChangeWorker)
|
||||
if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16), true, useChangeWorker)
|
||||
}
|
||||
}
|
||||
|
||||
queueAwaited = false
|
||||
messagesQueue = {} as { [workerIndex: string]: any[] }
|
||||
|
||||
setSectionDirty (pos: Vec3, value = true) { // value false is used for unloading chunks
|
||||
getWorkerNumber (pos: Vec3) {
|
||||
const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length - 1)
|
||||
return hash + 1
|
||||
}
|
||||
|
||||
setSectionDirty (pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks
|
||||
if (this.viewDistance === -1) throw new Error('viewDistance not set')
|
||||
this.allChunksFinished = false
|
||||
const distance = this.getDistance(pos)
|
||||
|
|
@ -397,8 +448,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
// Dispatch sections to workers based on position
|
||||
// This guarantees uniformity accross workers and that a given section
|
||||
// is always dispatched to the same worker
|
||||
const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length)
|
||||
this.sectionsOutstanding.set(key, (this.sectionsOutstanding.get(key) ?? 0) + 1)
|
||||
const hash = useChangeWorker ? 0 : this.getWorkerNumber(pos)
|
||||
this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
|
||||
this.messagesQueue[hash] ??= []
|
||||
this.messagesQueue[hash].push({
|
||||
// this.workers[hash].postMessage({
|
||||
|
|
@ -430,13 +481,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
// of sections not rendered are 0
|
||||
async waitForChunksToRender () {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if ([...this.sectionsOutstanding].length === 0) {
|
||||
if ([...this.sectionsWaiting].length === 0) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const updateHandler = () => {
|
||||
if (this.sectionsOutstanding.size === 0) {
|
||||
if (this.sectionsWaiting.size === 0) {
|
||||
this.renderUpdateEmitter.removeListener('update', updateHandler)
|
||||
resolve()
|
||||
}
|
||||
|
|
@ -444,4 +495,27 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.renderUpdateEmitter.on('update', updateHandler)
|
||||
})
|
||||
}
|
||||
|
||||
async waitForChunkToLoad (pos: Vec3) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
||||
if (this.loadedChunks[key]) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const updateHandler = () => {
|
||||
if (this.loadedChunks[key]) {
|
||||
this.renderUpdateEmitter.removeListener('update', updateHandler)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
this.renderUpdateEmitter.on('update', updateHandler)
|
||||
})
|
||||
}
|
||||
|
||||
destroy () {
|
||||
console.warn('world destroy is not implemented')
|
||||
}
|
||||
|
||||
abstract setHighlightCursorBlock (block: typeof this.cursorBlock, shapePositions?: Array<{ position; width; height; depth }>): void
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Vec3 } from 'vec3'
|
|||
import nbt from 'prismarine-nbt'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass } from 'three-stdlib'
|
||||
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass, LineSegmentsGeometry, Wireframe, LineMaterial } from 'three-stdlib'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { renderSign } from '../sign-renderer'
|
||||
import { chunkPos, sectionPos } from './simpleUtils'
|
||||
|
|
@ -14,6 +14,7 @@ import { addNewStat } from './ui/newStats'
|
|||
import { MesherGeometryOutput } from './mesher/shared'
|
||||
|
||||
export class WorldRendererThree extends WorldRendererCommon {
|
||||
interactionLines: null | { blockPos; mesh } = null
|
||||
outputFormat = 'threeJs' as const
|
||||
blockEntities = {}
|
||||
sectionObjects: Record<string, THREE.Object3D> = {}
|
||||
|
|
@ -22,6 +23,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
starField: StarField
|
||||
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
|
||||
holdingBlock: HoldingBlock
|
||||
holdingBlockLeft: HoldingBlock
|
||||
rendererDevice = '...'
|
||||
|
||||
get tilesRendered () {
|
||||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
|
||||
|
|
@ -33,35 +36,53 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) {
|
||||
super(config)
|
||||
this.rendererDevice = String(WorldRendererThree.getRendererInfo(this.renderer))
|
||||
this.starField = new StarField(scene)
|
||||
this.holdingBlock = new HoldingBlock(this.scene)
|
||||
this.holdingBlock = new HoldingBlock()
|
||||
this.holdingBlockLeft = new HoldingBlock()
|
||||
this.holdingBlockLeft.rightSide = false
|
||||
|
||||
this.renderUpdateEmitter.on('textureDownloaded', () => {
|
||||
this.renderUpdateEmitter.on('itemsTextureDownloaded', () => {
|
||||
if (this.holdingBlock.toBeRenderedItem) {
|
||||
this.onHandItemSwitch(this.holdingBlock.toBeRenderedItem)
|
||||
this.holdingBlock.toBeRenderedItem = undefined
|
||||
}
|
||||
if (this.holdingBlockLeft.toBeRenderedItem) {
|
||||
this.onHandItemSwitch(this.holdingBlock.toBeRenderedItem, true)
|
||||
this.holdingBlockLeft.toBeRenderedItem = undefined
|
||||
}
|
||||
})
|
||||
|
||||
this.addDebugOverlay()
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined) {
|
||||
onHandItemSwitch (item: HandItemBlock | undefined, isLeft = false) {
|
||||
if (!isLeft) {
|
||||
item ??= {
|
||||
type: 'hand',
|
||||
}
|
||||
}
|
||||
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
||||
if (!this.currentTextureImage) {
|
||||
this.holdingBlock.toBeRenderedItem = item
|
||||
holdingBlock.toBeRenderedItem = item
|
||||
return
|
||||
}
|
||||
void this.holdingBlock.initHandObject(this.material, this.blockstatesModels, this.blocksAtlases, item)
|
||||
void holdingBlock.initHandObject(this.material, this.blockstatesModels, this.blocksAtlases, item)
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean) {
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
||||
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
||||
if (isAnimationPlaying) {
|
||||
this.holdingBlock.startSwing()
|
||||
holdingBlock.startSwing()
|
||||
} else {
|
||||
void this.holdingBlock.stopSwing()
|
||||
void holdingBlock.stopSwing()
|
||||
}
|
||||
}
|
||||
|
||||
changeBackgroundColor (color: [number, number, number]): void {
|
||||
this.scene.background = new THREE.Color(color[0], color[1], color[2])
|
||||
}
|
||||
|
||||
timeUpdated (newTime: number): void {
|
||||
const nightTime = 13_500
|
||||
const morningStart = 23_000
|
||||
|
|
@ -216,10 +237,13 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
render () {
|
||||
tweenJs.update()
|
||||
this.holdingBlock.update(this.camera)
|
||||
// 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.displayHand) {
|
||||
this.holdingBlock.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
this.holdingBlockLeft.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
}
|
||||
}
|
||||
|
||||
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
||||
|
|
@ -361,9 +385,47 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
}
|
||||
|
||||
setSectionDirty (pos, value = true) {
|
||||
setSectionDirty (...args: Parameters<WorldRendererCommon['setSectionDirty']>) {
|
||||
const [pos] = args
|
||||
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
|
||||
super.setSectionDirty(pos, value)
|
||||
super.setSectionDirty(...args)
|
||||
}
|
||||
|
||||
setHighlightCursorBlock (blockPos: typeof this.cursorBlock, shapePositions?: Array<{ position: any; width: any; height: any; depth: any; }>): void {
|
||||
this.cursorBlock = blockPos
|
||||
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
|
||||
return
|
||||
}
|
||||
if (this.interactionLines !== null) {
|
||||
this.scene.remove(this.interactionLines.mesh)
|
||||
this.interactionLines = null
|
||||
}
|
||||
if (blockPos === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
for (const { position, width, height, depth } of shapePositions ?? []) {
|
||||
const scale = [1.0001 * width, 1.0001 * height, 1.0001 * depth] as const
|
||||
const geometry = new THREE.BoxGeometry(...scale)
|
||||
const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry))
|
||||
const wireframe = new Wireframe(lines, this.threejsCursorLineMaterial)
|
||||
const pos = blockPos.plus(position)
|
||||
wireframe.position.set(pos.x, pos.y, pos.z)
|
||||
wireframe.computeLineDistances()
|
||||
group.add(wireframe)
|
||||
}
|
||||
this.scene.add(group)
|
||||
this.interactionLines = { blockPos, mesh: group }
|
||||
}
|
||||
|
||||
static getRendererInfo (renderer: THREE.WebGLRenderer) {
|
||||
try {
|
||||
const gl = renderer.getContext()
|
||||
return `${gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL)} powered by three.js r{THREE.REVISION}`
|
||||
} catch (err) {
|
||||
console.warn('Failed to get renderer info', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ const appConfig = defineConfig({
|
|||
if (fs.existsSync('./prismarine-viewer/dist/mesher.js') && dev) {
|
||||
// copy mesher
|
||||
fs.copyFileSync('./prismarine-viewer/dist/mesher.js', './dist/mesher.js')
|
||||
fs.copyFileSync('./prismarine-viewer/dist/mesher.js.map', './dist/mesher.js.map')
|
||||
} else if (!dev) {
|
||||
await execAsync('pnpm run build-mesher')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import minecraftData from 'minecraft-data'
|
||||
import fs from 'fs'
|
||||
import supportedVersions from '../src/supportedVersions.mjs'
|
||||
|
||||
const data = minecraftData('1.20.1')
|
||||
|
||||
|
|
@ -10,4 +11,41 @@ types += `\nexport type EntityNames = ${Object.keys(data.entitiesByName).map(blo
|
|||
types += `\nexport type BiomesNames = ${Object.keys(data.biomesByName).map(blockName => `'${blockName}'`).join(' | ')};`
|
||||
types += `\nexport type EnchantmentNames = ${Object.keys(data.enchantmentsByName).map(blockName => `'${blockName}'`).join(' | ')};`
|
||||
|
||||
type Version = string
|
||||
const allVersionsEntitiesMetadata = {} as Record<string, Record<string, {
|
||||
version: Version,
|
||||
firstKey: number
|
||||
}>>
|
||||
for (const version of supportedVersions) {
|
||||
const data = minecraftData(version)
|
||||
for (const { name, metadataKeys } of data.entitiesArray) {
|
||||
allVersionsEntitiesMetadata[name] ??= {}
|
||||
if (!metadataKeys) {
|
||||
// console.warn('Entity has no metadata', name, version)
|
||||
}
|
||||
for (const [i, key] of (metadataKeys ?? []).entries()) {
|
||||
allVersionsEntitiesMetadata[name][key] ??= {
|
||||
version: version,
|
||||
firstKey: i,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
types += '\n\nexport type EntityMetadataVersions = {\n'
|
||||
for (const [name, versions] of Object.entries(allVersionsEntitiesMetadata)) {
|
||||
types += `'${name}': {`
|
||||
for (const [key, v] of Object.entries(versions)) {
|
||||
types += `\n/** ${v.version}+ (${v.firstKey}) */\n`
|
||||
types += `'${key}': string;`
|
||||
}
|
||||
types += '},'
|
||||
}
|
||||
types += '\n}'
|
||||
|
||||
const minify = false
|
||||
if (minify) {
|
||||
types = types.replaceAll(/[\t]/g, '')
|
||||
}
|
||||
|
||||
fs.writeFileSync('./src/mcDataTypes.ts', types, 'utf8')
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@ export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: stri
|
|||
return
|
||||
}
|
||||
if (!stat.isDirectory()) {
|
||||
await fs.promises.writeFile(pathDest, await fs.promises.readFile(pathSrc))
|
||||
await fs.promises.writeFile(pathDest, await fs.promises.readFile(pathSrc) as any)
|
||||
console.debug('copied single file', pathSrc, pathDest)
|
||||
return
|
||||
}
|
||||
|
|
@ -464,7 +464,7 @@ export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopi
|
|||
} else {
|
||||
// Copy file
|
||||
try {
|
||||
await fs.promises.writeFile(curPathDest, await fs.promises.readFile(curPathSrc))
|
||||
await fs.promises.writeFile(curPathDest, await fs.promises.readFile(curPathSrc) as any)
|
||||
console.debug('copied file', curPathSrc, curPathDest)
|
||||
} catch (err) {
|
||||
console.error('Error copying file', curPathSrc, curPathDest, err)
|
||||
|
|
@ -475,17 +475,36 @@ export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopi
|
|||
}))
|
||||
}
|
||||
|
||||
export const openWorldFromHttpDir = async (fileDescriptorUrl: string/* | undefined */, baseUrl = fileDescriptorUrl.split('/').slice(0, -1).join('/')) => {
|
||||
export const openWorldFromHttpDir = async (fileDescriptorUrls: string[]/* | undefined */, baseUrlParam) => {
|
||||
// todo try go guess mode
|
||||
let index
|
||||
const file = await fetch(fileDescriptorUrl).then(async a => a.json())
|
||||
if (file.baseUrl) {
|
||||
baseUrl = new URL(file.baseUrl, baseUrl).toString()
|
||||
index = file.index
|
||||
} else {
|
||||
index = file
|
||||
let baseUrl
|
||||
for (const url of fileDescriptorUrls) {
|
||||
let file
|
||||
try {
|
||||
setLoadingScreenStatus(`Trying to get world descriptor from ${new URL(url).host}`)
|
||||
const controller = new AbortController()
|
||||
setTimeout(() => {
|
||||
controller.abort()
|
||||
}, 3000)
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await fetch(url, { signal: controller.signal })
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
file = await response.json()
|
||||
} catch (err) {
|
||||
console.error('Error fetching file descriptor', url, err)
|
||||
}
|
||||
if (!file) continue
|
||||
if (file.baseUrl) {
|
||||
baseUrl = new URL(file.baseUrl, baseUrl).toString()
|
||||
index = file.index
|
||||
} else {
|
||||
index = file
|
||||
baseUrl = baseUrlParam ?? url.split('/').slice(0, -1).join('/')
|
||||
}
|
||||
break
|
||||
}
|
||||
if (!index) throw new Error(`The provided mapDir file is not valid descriptor file! ${fileDescriptorUrl}`)
|
||||
if (!index) throw new Error(`The provided mapDir file is not valid descriptor file! ${fileDescriptorUrls.join(', ')}`)
|
||||
await new Promise<void>(async resolve => {
|
||||
browserfs.configure({
|
||||
fs: 'MountableFileSystem',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ 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, loadedGameState } from './globalState'
|
||||
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, loadedGameState, hideModal } from './globalState'
|
||||
import { goFullscreen, pointerLock, reloadChunks } from './utils'
|
||||
import { options } from './optionsStorage'
|
||||
import { openPlayerInventory } from './inventoryWindows'
|
||||
|
|
@ -54,6 +54,7 @@ export const contro = new ControMax({
|
|||
ui: {
|
||||
toggleFullscreen: ['F11'],
|
||||
back: [null/* 'Escape' */, 'B'],
|
||||
toggleMap: ['KeyM'],
|
||||
leftClick: [null, 'A'],
|
||||
rightClick: [null, 'Y'],
|
||||
speedupCursor: [null, 'Left Stick'],
|
||||
|
|
@ -299,7 +300,7 @@ const alwaysPressedHandledCommand = (command: Command) => {
|
|||
}
|
||||
}
|
||||
|
||||
function lockUrl () {
|
||||
export function lockUrl () {
|
||||
let newQs = ''
|
||||
if (fsState.saveLoaded) {
|
||||
const save = localServer!.options.worldFolder.split('/').at(-1)
|
||||
|
|
@ -424,6 +425,14 @@ contro.on('trigger', ({ command }) => {
|
|||
if (command === 'ui.toggleFullscreen') {
|
||||
void goFullscreen(true)
|
||||
}
|
||||
|
||||
if (command === 'ui.toggleMap') {
|
||||
if (activeModalStack.at(-1)?.reactType === 'full-map') {
|
||||
hideModal({ reactType: 'full-map' })
|
||||
} else {
|
||||
showModal({ reactType: 'full-map' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
contro.on('release', ({ command }) => {
|
||||
|
|
@ -514,6 +523,15 @@ export const f3Keybinds = [
|
|||
}
|
||||
},
|
||||
mobileTitle: 'Cycle Game Mode'
|
||||
},
|
||||
{
|
||||
key: 'KeyP',
|
||||
async action () {
|
||||
const { uuid, ping: playerPing, username } = bot.player
|
||||
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'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,6 @@ module.exports = {
|
|||
keepAlive: false,
|
||||
'everybody-op': true,
|
||||
'max-entities': 100,
|
||||
'version': '1.14.4',
|
||||
versionMajor: '1.14'
|
||||
'version': '1.18.2',
|
||||
versionMajor: '1.18'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ customEvents.on('gameLoaded', () => {
|
|||
window.inspectPacket = (packetName, full = false) => {
|
||||
const listener = (...args) => console.log('packet', packetName, full ? args : args[0])
|
||||
const attach = () => {
|
||||
bot?._client.on(packetName, listener)
|
||||
bot?._client.prependListener(packetName, listener)
|
||||
}
|
||||
attach()
|
||||
customEvents.on('mineflayerBotCreated', attach)
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ export const getFixedFilesize = (bytes: number) => {
|
|||
|
||||
const inner = async () => {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
const mapUrlDir = qs.get('mapDir')
|
||||
const mapUrlDir = qs.getAll('mapDir')
|
||||
const mapUrlDirGuess = qs.get('mapDirGuess')
|
||||
const mapUrlDirBaseUrl = qs.get('mapDirBaseUrl')
|
||||
if (mapUrlDir) {
|
||||
if (mapUrlDir.length) {
|
||||
await openWorldFromHttpDir(mapUrlDir, mapUrlDirBaseUrl ?? undefined)
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ export async function savePlayers (autoSave: boolean) {
|
|||
export const saveServer = async (autoSave = true) => {
|
||||
if (!localServer || fsState.isReadonly) return
|
||||
// todo
|
||||
console.time('save server')
|
||||
const worlds = [(localServer as any).overworld] as Array<import('prismarine-world').world.World>
|
||||
await Promise.all([localServer.writeLevelDat(), savePlayers(autoSave), ...worlds.map(async world => world.saveNow())])
|
||||
console.timeEnd('save server')
|
||||
}
|
||||
export const disconnect = async () => {
|
||||
if (localServer) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//@ts-check
|
||||
|
||||
import { proxy, ref, subscribe } from 'valtio'
|
||||
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import { pointerLock } from './utils'
|
||||
import type { OptionsGroupType } from './optionsGuiScheme'
|
||||
|
||||
|
|
@ -153,6 +154,7 @@ export const gameAdditionalState = proxy({
|
|||
isFlying: false,
|
||||
isSprinting: false,
|
||||
isSneaking: false,
|
||||
warps: [] as WorldWarp[]
|
||||
})
|
||||
|
||||
window.gameAdditionalState = gameAdditionalState
|
||||
|
|
|
|||
12
src/index.ts
12
src/index.ts
|
|
@ -22,7 +22,7 @@ import PrismarineItem from 'prismarine-item'
|
|||
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import './reactUi'
|
||||
import { contro, onBotCreate } from './controls'
|
||||
import { contro, lockUrl, onBotCreate } from './controls'
|
||||
import './dragndrop'
|
||||
import { possiblyCleanHandle, resetStateAfterDisconnect } from './browserfs'
|
||||
import { watchOptionsAfterViewerInit, watchOptionsAfterWorldViewInit } from './watchOptions'
|
||||
|
|
@ -157,6 +157,7 @@ if (isIphone) {
|
|||
// Create viewer
|
||||
const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer = new Viewer(renderer)
|
||||
window.viewer = viewer
|
||||
viewer.getMineflayerBot = () => bot
|
||||
// todo unify
|
||||
viewer.entities.getItemUv = (idOrName: number | string) => {
|
||||
try {
|
||||
|
|
@ -420,7 +421,7 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
}
|
||||
}
|
||||
viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json')
|
||||
viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
|
||||
void viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
|
||||
}
|
||||
|
||||
const downloadVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined)
|
||||
|
|
@ -692,6 +693,9 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
setLoadingScreenStatus('Placing blocks (starting viewer)')
|
||||
localStorage.lastConnectOptions = JSON.stringify(connectOptions)
|
||||
connectOptions.onSuccessfulPlay?.()
|
||||
if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && new URLSearchParams(location.search).size === 0) {
|
||||
lockUrl()
|
||||
}
|
||||
updateDataAfterJoin()
|
||||
if (connectOptions.autoLoginPassword) {
|
||||
bot.chat(`/login ${connectOptions.autoLoginPassword}`)
|
||||
|
|
@ -1046,6 +1050,10 @@ downloadAndOpenFile().then((downloadAction) => {
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (qs.get('serversList')) {
|
||||
showModal({ reactType: 'serversList' })
|
||||
}
|
||||
}, (err) => {
|
||||
console.error(err)
|
||||
alert(`Failed to download file: ${err}`)
|
||||
|
|
|
|||
4322
src/mcDataTypes.ts
4322
src/mcDataTypes.ts
File diff suppressed because one or more lines are too long
|
|
@ -89,7 +89,6 @@ export const guiOptionsScheme: {
|
|||
tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far',
|
||||
},
|
||||
handDisplay: {},
|
||||
neighborChunkUpdates: {},
|
||||
renderDebug: {
|
||||
values: [
|
||||
'advanced',
|
||||
|
|
@ -250,6 +249,19 @@ export const guiOptionsScheme: {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Category>Map</Category>
|
||||
},
|
||||
showMinimap: {
|
||||
text: 'Enable Minimap',
|
||||
values: [
|
||||
'always',
|
||||
'singleplayer',
|
||||
'never'
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Category>Experimental</Category>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ const defaultOptions = {
|
|||
autoParkour: false,
|
||||
vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users
|
||||
renderDebug: (isDev ? 'advanced' : 'basic') as 'none' | 'advanced' | 'basic',
|
||||
autoVersionSelect: '1.20.4',
|
||||
|
||||
// advanced bot options
|
||||
autoRespawn: false,
|
||||
|
|
@ -88,6 +89,8 @@ const defaultOptions = {
|
|||
/** Wether to popup sign editor on server action */
|
||||
autoSignEditor: true,
|
||||
wysiwygSignEditor: 'auto' as 'auto' | 'always' | 'never',
|
||||
showMinimap: 'never' as 'always' | 'singleplayer' | 'never',
|
||||
minimapOptimizations: true,
|
||||
displayBossBars: false, // boss bar overlay was removed for some reason, enable safely
|
||||
disabledUiParts: [] as string[],
|
||||
neighborChunkUpdates: true
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ export default ({
|
|||
return (
|
||||
<Screen
|
||||
className='small-content'
|
||||
titleSelectable={isError}
|
||||
title={
|
||||
<>
|
||||
<span style={{
|
||||
userSelect: isError ? 'text' : undefined,
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useEffect, useRef, useMemo, useState } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import type { Block } from 'prismarine-block'
|
||||
import { getFixedFilesize } from '../downloadAndOpenFile'
|
||||
import { options } from '../optionsStorage'
|
||||
import worldInteractions from '../worldInteractions'
|
||||
import styles from './DebugOverlay.module.css'
|
||||
|
||||
export default () => {
|
||||
|
|
@ -35,10 +35,10 @@ export default () => {
|
|||
const [day, setDay] = useState(0)
|
||||
const [entitiesCount, setEntitiesCount] = useState(0)
|
||||
const [dimension, setDimension] = useState('')
|
||||
const [cursorBlock, setCursorBlock] = useState<typeof worldInteractions.cursorBlock>(null)
|
||||
const [rendererDevice, setRendererDevice] = useState('')
|
||||
const [cursorBlock, setCursorBlock] = useState<Block | null>(null)
|
||||
const minecraftYaw = useRef(0)
|
||||
const minecraftQuad = useRef(0)
|
||||
const { rendererDevice } = viewer.world
|
||||
|
||||
const quadsDescription = [
|
||||
'north (towards negative Z)',
|
||||
|
|
@ -105,7 +105,7 @@ export default () => {
|
|||
setBiomeId(bot.world.getBiome(bot.entity.position))
|
||||
setDimension(bot.game.dimension)
|
||||
setDay(bot.time.day)
|
||||
setCursorBlock(worldInteractions.cursorBlock)
|
||||
setCursorBlock(bot.blockAtCursor(5))
|
||||
setEntitiesCount(Object.values(bot.entities).length)
|
||||
}, 100)
|
||||
|
||||
|
|
@ -118,13 +118,6 @@ export default () => {
|
|||
managePackets('sent', name, data)
|
||||
})
|
||||
|
||||
try {
|
||||
const gl = window.renderer.getContext()
|
||||
setRendererDevice(gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL))
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleF3)
|
||||
clearInterval(packetsUpdateInterval)
|
||||
|
|
@ -159,7 +152,7 @@ export default () => {
|
|||
</div>
|
||||
|
||||
<div className={styles['debug-right-side']}>
|
||||
<p>Renderer: {rendererDevice} powered by three.js r{THREE.REVISION}</p>
|
||||
<p>Renderer: {rendererDevice}</p>
|
||||
<div className={styles.empty} />
|
||||
{cursorBlock ? (<>
|
||||
<p>{cursorBlock.name}</p>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default ({
|
|||
|
||||
useEffect(() => {
|
||||
if (foodRef.current) {
|
||||
foodRef.current.classList.toggle('creative', gameMode === 'creative')
|
||||
foodRef.current.classList.toggle('creative', gameMode === 'creative' || gameMode === 'spectator')
|
||||
}
|
||||
}, [gameMode])
|
||||
|
||||
|
|
|
|||
13
src/react/Fullmap.css
Normal file
13
src/react/Fullmap.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
.map {
|
||||
width: 70% !important;
|
||||
height: 80% !important;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.map {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
512
src/react/Fullmap.tsx
Normal file
512
src/react/Fullmap.tsx
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import { useRef, useEffect, useState, CSSProperties, Dispatch, SetStateAction } from 'react'
|
||||
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'
|
||||
import { MinimapDrawer, DrawerAdapter, ChunkInfo } from './MinimapDrawer'
|
||||
import Button from './Button'
|
||||
import Input from './Input'
|
||||
import './Fullmap.css'
|
||||
|
||||
|
||||
type FullmapProps = {
|
||||
toggleFullMap: () => void,
|
||||
adapter: DrawerAdapter,
|
||||
drawer: MinimapDrawer | null,
|
||||
canvasRef: any
|
||||
}
|
||||
|
||||
export default ({ toggleFullMap, adapter }: FullmapProps) => {
|
||||
const [grid, setGrid] = useState(() => new Set<string>())
|
||||
const zoomRef = useRef<ReactZoomPanPinchRef>(null)
|
||||
const redrawCell = useRef(false)
|
||||
const [lastWarpPos, setLastWarpPos] = useState({ x: 0, y: 0, z: 0 })
|
||||
const stateRef = useRef({ scale: 1, positionX: 0, positionY: 0 })
|
||||
const cells = useRef({ columns: 0, rows: 0 })
|
||||
const [isWarpInfoOpened, setIsWarpInfoOpened] = useState(false)
|
||||
const [initWarp, setInitWarp] = useState<WorldWarp | undefined>(undefined)
|
||||
const [warpPreview, setWarpPreview] = useState<{ name: string, x: number, z: number, clientX: number, clientY: number } | undefined>(undefined)
|
||||
|
||||
const updateGrid = () => {
|
||||
const wrapperRect = zoomRef.current?.instance.wrapperComponent?.getBoundingClientRect()
|
||||
if (!wrapperRect) return
|
||||
const cellSize = 64
|
||||
const columns = Math.ceil(wrapperRect.width / (cellSize * stateRef.current.scale))
|
||||
const rows = Math.ceil(wrapperRect.height / (cellSize * stateRef.current.scale))
|
||||
cells.current.rows = rows
|
||||
cells.current.columns = columns
|
||||
const leftBorder = - Math.floor(stateRef.current.positionX / (stateRef.current.scale * cellSize)) * cellSize
|
||||
const topBorder = - Math.floor(stateRef.current.positionY / (stateRef.current.scale * cellSize)) * cellSize
|
||||
const newGrid = new Set<string>()
|
||||
for (let row = -1; row < rows; row += 1) {
|
||||
for (let col = -1; col < columns; col += 1) {
|
||||
const x = leftBorder + col * cellSize
|
||||
const y = topBorder + row * cellSize
|
||||
newGrid.add(`${x},${y}`)
|
||||
}
|
||||
}
|
||||
setGrid(newGrid)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
adapter.full = true
|
||||
console.log('[fullmap] set full property to true')
|
||||
updateGrid()
|
||||
}, [])
|
||||
|
||||
return <div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
isolation: 'isolate',
|
||||
inset: '0px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
zIndex: 100
|
||||
}}
|
||||
>
|
||||
{window.screen.width > 500 ? <div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: '-1'
|
||||
}}
|
||||
onClick={toggleFullMap}
|
||||
/>
|
||||
: <Button
|
||||
icon="close-box"
|
||||
onClick={toggleFullMap}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
zIndex: 1
|
||||
}}
|
||||
/>}
|
||||
<TransformWrapper
|
||||
limitToBounds={false}
|
||||
ref={zoomRef}
|
||||
minScale={0.1}
|
||||
doubleClick={{
|
||||
disabled: false
|
||||
}}
|
||||
panning={{
|
||||
allowLeftClickPan: true,
|
||||
allowRightClickPan: false
|
||||
}}
|
||||
onTransformed={(ref, state) => {
|
||||
stateRef.current = { ...state }
|
||||
}}
|
||||
onPanningStop={() => {
|
||||
updateGrid()
|
||||
}}
|
||||
onZoomStop={() => {
|
||||
updateGrid()
|
||||
}}
|
||||
>
|
||||
<TransformComponent
|
||||
wrapperClass="map"
|
||||
wrapperStyle={{
|
||||
willChange: 'transform',
|
||||
}}
|
||||
>
|
||||
{[...grid].map((cellCoords) => {
|
||||
const [x, y] = cellCoords.split(',').map(Number)
|
||||
const playerChunkLeft = Math.floor(adapter.playerPosition.x / 16) * 16
|
||||
const playerChunkTop = Math.floor(adapter.playerPosition.z / 16) * 16
|
||||
const wrapperRect = zoomRef.current?.instance.wrapperComponent?.getBoundingClientRect()
|
||||
const offsetX = Math.floor((wrapperRect?.width ?? 0) / (8 * 16)) * 16
|
||||
const offsetY = Math.floor((wrapperRect?.height ?? 0) / (8 * 16)) * 16
|
||||
|
||||
return <MapChunk
|
||||
key={'mapcell:' + cellCoords}
|
||||
x={x}
|
||||
y={y}
|
||||
scale={stateRef.current.scale}
|
||||
adapter={adapter}
|
||||
worldX={playerChunkLeft + x / 4 - offsetX}
|
||||
worldZ={playerChunkTop + y / 4 - offsetY}
|
||||
setIsWarpInfoOpened={setIsWarpInfoOpened}
|
||||
setLastWarpPos={setLastWarpPos}
|
||||
redraw={redrawCell.current}
|
||||
setInitWarp={setInitWarp}
|
||||
setWarpPreview={setWarpPreview}
|
||||
/>
|
||||
})}
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
{warpPreview && <div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: warpPreview.clientY - 70,
|
||||
left: warpPreview.clientX - 70,
|
||||
textAlign: 'center',
|
||||
fontSize: '1.5em',
|
||||
textShadow: '0.1em 0 black, 0 0.1em black, -0.1em 0 black, 0 -0.1em black, -0.1em -0.1em black, -0.1em 0.1em black, 0.1em -0.1em black, 0.1em 0.1em black'
|
||||
} as any}
|
||||
>
|
||||
{warpPreview.name}
|
||||
<div>
|
||||
{warpPreview.x} {warpPreview.z}
|
||||
</div>
|
||||
</div>}
|
||||
{
|
||||
isWarpInfoOpened && <WarpInfo
|
||||
adapter={adapter}
|
||||
warpPos={lastWarpPos}
|
||||
setIsWarpInfoOpened={setIsWarpInfoOpened}
|
||||
afterWarpIsSet={() => {
|
||||
redrawCell.current = !redrawCell.current
|
||||
}}
|
||||
initWarp={initWarp}
|
||||
setInitWarp={setInitWarp}
|
||||
toggleFullMap={toggleFullMap}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const MapChunk = (
|
||||
{ x, y, scale, adapter, worldX, worldZ, setIsWarpInfoOpened, setLastWarpPos, redraw, setInitWarp, setWarpPreview }:
|
||||
{
|
||||
x: number,
|
||||
y: number,
|
||||
scale: number,
|
||||
adapter: DrawerAdapter,
|
||||
worldX: number,
|
||||
worldZ: number,
|
||||
setIsWarpInfoOpened: (x: boolean) => void,
|
||||
setLastWarpPos: (obj: { x: number, y: number, z: number }) => void,
|
||||
redraw?: boolean
|
||||
setInitWarp?: (warp: WorldWarp | undefined) => void
|
||||
setWarpPreview?: (warpInfo) => void
|
||||
}
|
||||
) => {
|
||||
const containerRef = useRef(null)
|
||||
const drawerRef = useRef<MinimapDrawer | null>(null)
|
||||
const touchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const [isCanvas, setIsCanvas] = useState(false)
|
||||
|
||||
const longPress = (e) => {
|
||||
touchTimer.current = setTimeout(() => {
|
||||
touchTimer.current = null
|
||||
handleClick(e)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
if (touchTimer.current) clearTimeout(touchTimer.current)
|
||||
}
|
||||
|
||||
const handleClick = (e: MouseEvent | TouchEvent) => {
|
||||
// console.log('click:', e)
|
||||
if (!drawerRef.current) return
|
||||
let clientX: number
|
||||
let clientY: number
|
||||
if ('buttons' in e && e.button === 2) {
|
||||
clientX = e.clientX
|
||||
clientY = e.clientY
|
||||
} else if ('changedTouches' in e) {
|
||||
clientX = (e).changedTouches[0].clientX
|
||||
clientY = (e).changedTouches[0].clientY
|
||||
} else { return }
|
||||
const [x, z] = getXZ(clientX, clientY)
|
||||
const mapX = Math.floor(x + worldX)
|
||||
const mapZ = Math.floor(z + worldZ)
|
||||
const y = adapter.getHighestBlockY(mapX, mapZ)
|
||||
drawerRef.current.setWarpPosOnClick(new Vec3(mapX, y, mapZ))
|
||||
setLastWarpPos(drawerRef.current.lastWarpPos)
|
||||
const { lastWarpPos } = drawerRef.current
|
||||
const initWarp = adapter.warps.find(warp => Math.hypot(lastWarpPos.x - warp.x, lastWarpPos.z - warp.z) < 2)
|
||||
setInitWarp?.(initWarp)
|
||||
setIsWarpInfoOpened(true)
|
||||
}
|
||||
|
||||
const getXZ = (clientX: number, clientY: number) => {
|
||||
const rect = canvasRef.current!.getBoundingClientRect()
|
||||
const factor = scale * (drawerRef.current?.mapPixel ?? 1)
|
||||
const x = (clientX - rect.left) / factor
|
||||
const y = (clientY - rect.top) / factor
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const [x, z] = getXZ(e.clientX, e.clientY)
|
||||
const warp = adapter.warps.find(w => Math.hypot(w.x - x - worldX, w.z - z - worldZ) < 2)
|
||||
setWarpPreview?.(
|
||||
warp ? { name: warp.name, x: warp.x, z: warp.z, clientX: e.clientX, clientY: e.clientY } : undefined
|
||||
)
|
||||
}
|
||||
|
||||
const handleRedraw = (key?: string, chunk?: ChunkInfo) => {
|
||||
if (key !== `${worldX / 16},${worldZ / 16}`) return
|
||||
adapter.mapDrawer.canvas = canvasRef.current!
|
||||
adapter.mapDrawer.full = true
|
||||
// console.log('handle redraw:', key)
|
||||
// if (chunk) {
|
||||
// drawerRef.current?.chunksStore.set(key, chunk)
|
||||
// }
|
||||
if (!adapter.chunksStore.has(key)) {
|
||||
adapter.chunksStore.set(key, 'requested')
|
||||
void adapter.loadChunk(key)
|
||||
return
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
const center = new Vec3(worldX + 8, 0, worldZ + 8)
|
||||
drawerRef.current!.lastBotPos = center
|
||||
drawerRef.current?.drawChunk(key)
|
||||
// drawerRef.current?.drawWarps(center)
|
||||
// drawerRef.current?.drawPlayerPos(center.x, center.z)
|
||||
clearTimeout(timeout)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// if (canvasRef.current && !drawerRef.current) {
|
||||
// drawerRef.current = adapter.mapDrawer
|
||||
// } else if (canvasRef.current && drawerRef.current) {
|
||||
// }
|
||||
if (canvasRef.current) void adapter.drawChunkOnCanvas(`${worldX / 16},${worldZ / 16}`, canvasRef.current)
|
||||
}, [canvasRef.current])
|
||||
|
||||
useEffect(() => {
|
||||
canvasRef.current?.addEventListener('contextmenu', handleClick)
|
||||
canvasRef.current?.addEventListener('touchstart', longPress)
|
||||
canvasRef.current?.addEventListener('touchend', cancel)
|
||||
canvasRef.current?.addEventListener('touchmove', cancel)
|
||||
canvasRef.current?.addEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return () => {
|
||||
canvasRef.current?.removeEventListener('contextmenu', handleClick)
|
||||
canvasRef.current?.removeEventListener('touchstart', longPress)
|
||||
canvasRef.current?.removeEventListener('touchend', cancel)
|
||||
canvasRef.current?.removeEventListener('touchmove', cancel)
|
||||
canvasRef.current?.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, [canvasRef.current, scale])
|
||||
|
||||
useEffect(() => {
|
||||
// handleRedraw()
|
||||
}, [drawerRef.current, redraw])
|
||||
|
||||
useEffect(() => {
|
||||
const intersectionObserver = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
setIsCanvas(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
intersectionObserver.observe(containerRef.current!)
|
||||
|
||||
// adapter.on('chunkReady', handleRedraw)
|
||||
|
||||
return () => {
|
||||
intersectionObserver.disconnect()
|
||||
// adapter.off('chunkReady', handleRedraw)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
imageRendering: 'pixelated'
|
||||
}}
|
||||
width={64}
|
||||
height={64}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const WarpInfo = (
|
||||
{ adapter, warpPos, setIsWarpInfoOpened, afterWarpIsSet, initWarp, toggleFullMap }:
|
||||
{
|
||||
adapter: DrawerAdapter,
|
||||
warpPos: { x: number, y: number, z: number },
|
||||
setIsWarpInfoOpened: Dispatch<SetStateAction<boolean>>,
|
||||
afterWarpIsSet?: () => void
|
||||
initWarp?: WorldWarp,
|
||||
setInitWarp?: React.Dispatch<React.SetStateAction<WorldWarp | undefined>>,
|
||||
toggleFullMap?: ({ command }: { command: string }) => void
|
||||
}
|
||||
) => {
|
||||
const [warp, setWarp] = useState<WorldWarp>(initWarp ?? {
|
||||
name: '',
|
||||
x: warpPos?.x ?? 100,
|
||||
y: warpPos?.y ?? 100,
|
||||
z: warpPos?.z ?? 100,
|
||||
color: '',
|
||||
disabled: false,
|
||||
world: adapter.world
|
||||
})
|
||||
|
||||
const posInputStyle: CSSProperties = {
|
||||
flexGrow: '1',
|
||||
}
|
||||
const fieldCont: CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px'
|
||||
}
|
||||
|
||||
const updateChunk = () => {
|
||||
for (let i = -1; i < 2; i += 1) {
|
||||
for (let j = -1; j < 2; j += 1) {
|
||||
adapter.emit(
|
||||
'chunkReady',
|
||||
`${Math.floor(warp.x / 16) + j},${Math.floor(warp.z / 16) + i}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tpNow = () => {
|
||||
adapter.off('updateChunk', tpNow)
|
||||
}
|
||||
|
||||
const quickTp = () => {
|
||||
toggleFullMap?.({ command: 'ui.toggleMap' })
|
||||
adapter.quickTp?.(warp.x, warp.z)
|
||||
}
|
||||
|
||||
return <div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: '0px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
fontSize: '0.8em',
|
||||
transform: 'scale(2)'
|
||||
}}
|
||||
>
|
||||
<form
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
width: window.screen.width > 500 ? '100%' : '50%',
|
||||
minWidth: '100px',
|
||||
maxWidth: '300px',
|
||||
padding: '20px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
border: '2px solid black'
|
||||
}}
|
||||
>
|
||||
<h2 style={{ alignSelf: 'center' }}>Point on the map</h2>
|
||||
<div style={fieldCont}>
|
||||
<div>
|
||||
Name:
|
||||
</div>
|
||||
<Input
|
||||
defaultValue={warp.name}
|
||||
onChange={(e) => {
|
||||
if (!e.target) return
|
||||
setWarp(prev => { return { ...prev, name: e.target.value } })
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div style={fieldCont}>
|
||||
<div>
|
||||
X:
|
||||
</div>
|
||||
<Input
|
||||
rootStyles={posInputStyle}
|
||||
defaultValue={warp.x ?? 100}
|
||||
onChange={(e) => {
|
||||
if (!e.target) return
|
||||
setWarp(prev => { return { ...prev, x: Number(e.target.value) } })
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
Z:
|
||||
</div>
|
||||
<Input
|
||||
rootStyles={posInputStyle}
|
||||
defaultValue={warp.z ?? 100}
|
||||
onChange={(e) => {
|
||||
if (!e.target) return
|
||||
setWarp(prev => { return { ...prev, z: Number(e.target.value) } })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={fieldCont}>
|
||||
<div>Color:</div>
|
||||
<Input
|
||||
type='color'
|
||||
defaultValue={warp.color === '' ? '#232323' : warp.color}
|
||||
onChange={(e) => {
|
||||
if (!e.target) return
|
||||
setWarp(prev => { return { ...prev, color: e.target.value } })
|
||||
}}
|
||||
rootStyles={{ width: '30px', }}
|
||||
style={{ left: '0px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={fieldCont} >
|
||||
<label htmlFor='warp-disabled'>Disabled:</label>
|
||||
<input
|
||||
id='warp-disabled'
|
||||
type="checkbox"
|
||||
checked={warp.disabled ?? false}
|
||||
onChange={(e) => {
|
||||
if (!e.target) return
|
||||
setWarp(prev => { return { ...prev, disabled: e.target.checked } })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
style={{ alignSelf: 'center' }}
|
||||
onClick={() => {
|
||||
quickTp()
|
||||
}}
|
||||
>Quick TP</Button>
|
||||
<div style={fieldCont}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsWarpInfoOpened(false)
|
||||
}}
|
||||
>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
adapter.setWarp({ ...warp })
|
||||
console.log(adapter.warps)
|
||||
setIsWarpInfoOpened(false)
|
||||
updateChunk()
|
||||
afterWarpIsSet?.()
|
||||
}}
|
||||
type='submit'
|
||||
>Add Warp</Button>
|
||||
{initWarp && <Button
|
||||
onClick={() => {
|
||||
const index = adapter.warps.findIndex(thisWarp => thisWarp.name === warp.name)
|
||||
if (index !== -1) {
|
||||
adapter.setWarp({ name: warp.name, x: 0, y: 0, z: 0, color: '', disabled: false, world: '' }, true)
|
||||
setIsWarpInfoOpened(false)
|
||||
updateChunk()
|
||||
afterWarpIsSet?.()
|
||||
}
|
||||
}}
|
||||
>Delete</Button>}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ const ItemName = ({ itemKey }: { itemKey: string }) => {
|
|||
</Transition>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const Inner = () => {
|
||||
const container = useRef<HTMLDivElement>(null!)
|
||||
const [itemKey, setItemKey] = useState('')
|
||||
const hasModals = useSnapshot(activeModalStack).length
|
||||
|
|
@ -221,6 +221,17 @@ export default () => {
|
|||
</SharedHudVars>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [gameMode, setGameMode] = useState(bot.game?.gameMode ?? 'creative')
|
||||
useEffect(() => {
|
||||
bot.on('game', () => {
|
||||
setGameMode(bot.game.gameMode)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return gameMode === 'spectator' ? null : <Inner />
|
||||
}
|
||||
|
||||
const Portal = ({ children, to = document.body }) => {
|
||||
return createPortal(children, to)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ interface Props extends React.ComponentProps<'input'> {
|
|||
validateInput?: (value: string) => CSSProperties | undefined
|
||||
}
|
||||
|
||||
export default ({ autoFocus, rootStyles, inputRef, validateInput, ...inputProps }: Props) => {
|
||||
export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, ...inputProps }: Props) => {
|
||||
const ref = useRef<HTMLInputElement>(null!)
|
||||
const [validationStyle, setValidationStyle] = useState<CSSProperties>({})
|
||||
const [value, setValue] = useState(inputProps.value ?? '')
|
||||
const [value, setValue] = useState(defaultValue ?? '')
|
||||
|
||||
useEffect(() => {
|
||||
setValue(inputProps.value === '' || inputProps.value ? inputProps.value : value)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export default () => {
|
|||
return
|
||||
}
|
||||
const upStatus = () => {
|
||||
setVersionStatus(`(${isLatest ? 'latest' : 'new version available'}${mainMenuState.serviceWorkerLoaded ? ' - Available Offline' : ''})`)
|
||||
setVersionStatus(`(${isLatest ? 'latest' : 'new version available'}${mainMenuState.serviceWorkerLoaded ? ', Downloaded' : ''})`)
|
||||
}
|
||||
subscribe(mainMenuState, upStatus)
|
||||
upStatus()
|
||||
|
|
|
|||
74
src/react/Minimap.stories.tsx
Normal file
74
src/react/Minimap.stories.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import { TypedEventEmitter } from 'contro-max/build/typedEventEmitter'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import Minimap from './Minimap'
|
||||
import { DrawerAdapter, MapUpdates } from './MinimapDrawer'
|
||||
|
||||
const meta: Meta<typeof Minimap> = {
|
||||
component: Minimap,
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
|
||||
useEffect(() => {
|
||||
console.log('map updated')
|
||||
adapter.emit('updateMap')
|
||||
|
||||
}, [context.args['fullMap']])
|
||||
|
||||
return <div> <Story /> </div>
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof Minimap>
|
||||
|
||||
|
||||
class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> {
|
||||
playerPosition: Vec3
|
||||
yaw: number
|
||||
warps: WorldWarp[]
|
||||
chunksStore: any = {}
|
||||
full: boolean
|
||||
|
||||
constructor (pos?: Vec3, warps?: WorldWarp[]) {
|
||||
super()
|
||||
this.playerPosition = pos ?? new Vec3(0, 0, 0)
|
||||
this.warps = warps ?? [] as WorldWarp[]
|
||||
}
|
||||
|
||||
async getHighestBlockColor (x: number, z: number) {
|
||||
console.log('got color')
|
||||
return 'green'
|
||||
}
|
||||
|
||||
getHighestBlockY (x: number, z: number) {
|
||||
return 0
|
||||
}
|
||||
|
||||
setWarp (warp: WorldWarp, remove?: boolean): void {
|
||||
const index = this.warps.findIndex(w => w.name === warp.name)
|
||||
if (index === -1) {
|
||||
this.warps.push(warp)
|
||||
} else {
|
||||
this.warps[index] = warp
|
||||
}
|
||||
this.emit('updateWarps')
|
||||
}
|
||||
|
||||
clearChunksStore (x: number, z: number) { }
|
||||
|
||||
async loadChunk (key: string) {}
|
||||
}
|
||||
|
||||
const adapter = new DrawerAdapterImpl() as any
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
adapter,
|
||||
fullMap: false
|
||||
},
|
||||
}
|
||||
175
src/react/Minimap.tsx
Normal file
175
src/react/Minimap.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { useRef, useEffect, useState } from 'react'
|
||||
import { MinimapDrawer, DrawerAdapter, ChunkInfo } from './MinimapDrawer'
|
||||
import Fullmap from './Fullmap'
|
||||
|
||||
|
||||
export type DisplayMode = 'fullmapOnly' | 'minimapOnly'
|
||||
|
||||
export default (
|
||||
{ adapter, showMinimap, showFullmap, singleplayer, fullMap, toggleFullMap, displayMode }:
|
||||
{
|
||||
adapter: DrawerAdapter,
|
||||
showMinimap: string,
|
||||
showFullmap: string,
|
||||
singleplayer: boolean,
|
||||
fullMap?: boolean,
|
||||
toggleFullMap?: ({ command }: { command: string }) => void
|
||||
displayMode?: DisplayMode
|
||||
}
|
||||
) => {
|
||||
const full = useRef(false)
|
||||
const canvasTick = useRef(0)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const warpsAndPartsCanvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const playerPosCanvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const warpsDrawerRef = useRef<MinimapDrawer | null>(null)
|
||||
const drawerRef = useRef<MinimapDrawer | null>(null)
|
||||
const playerPosDrawerRef = useRef<MinimapDrawer | null>(null)
|
||||
const [position, setPosition] = useState({ x: 0, y: 0, z: 0 })
|
||||
|
||||
const updateMap = () => {
|
||||
setPosition({ x: adapter.playerPosition.x, y: adapter.playerPosition.y, z: adapter.playerPosition.z })
|
||||
if (drawerRef.current) {
|
||||
if (!full.current) {
|
||||
rotateMap()
|
||||
drawerRef.current.draw(adapter.playerPosition)
|
||||
drawerRef.current.drawPlayerPos()
|
||||
drawerRef.current.drawWarps()
|
||||
}
|
||||
if (canvasTick.current % 300 === 0 && !fullMap) {
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => {
|
||||
drawerRef.current?.clearChunksStore()
|
||||
})
|
||||
} else {
|
||||
drawerRef.current.clearChunksStore()
|
||||
}
|
||||
canvasTick.current = 0
|
||||
}
|
||||
}
|
||||
canvasTick.current += 1
|
||||
}
|
||||
|
||||
const updateWarps = () => { }
|
||||
|
||||
const rotateMap = () => {
|
||||
if (!drawerRef.current) return
|
||||
drawerRef.current.canvas.style.transform = `rotate(${adapter.yaw}rad)`
|
||||
if (!warpsDrawerRef.current) return
|
||||
warpsDrawerRef.current.canvas.style.transform = `rotate(${adapter.yaw}rad)`
|
||||
}
|
||||
|
||||
const updateChunkOnMap = (key: string, chunk: ChunkInfo) => {
|
||||
adapter.chunksStore.set(key, chunk)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current && !drawerRef.current) {
|
||||
drawerRef.current = adapter.mapDrawer
|
||||
drawerRef.current.canvas = canvasRef.current
|
||||
// drawerRef.current.adapter.on('chunkReady', updateChunkOnMap)
|
||||
} else if (canvasRef.current && drawerRef.current) {
|
||||
drawerRef.current.canvas = canvasRef.current
|
||||
}
|
||||
|
||||
}, [canvasRef.current])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (warpsAndPartsCanvasRef.current && !warpsDrawerRef.current) {
|
||||
// warpsDrawerRef.current = new MinimapDrawer(warpsAndPartsCanvasRef.current, adapter)
|
||||
// } else if (warpsAndPartsCanvasRef.current && warpsDrawerRef.current) {
|
||||
// warpsDrawerRef.current.canvas = warpsAndPartsCanvasRef.current
|
||||
// }
|
||||
// }, [warpsAndPartsCanvasRef.current])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (playerPosCanvasRef.current && !playerPosDrawerRef.current) {
|
||||
// playerPosDrawerRef.current = new MinimapDrawer(playerPosCanvasRef.current, adapter)
|
||||
// } else if (playerPosCanvasRef.current && playerPosDrawerRef.current) {
|
||||
// playerPosDrawerRef.current.canvas = playerPosCanvasRef.current
|
||||
// }
|
||||
// }, [playerPosCanvasRef.current])
|
||||
|
||||
useEffect(() => {
|
||||
adapter.on('updateMap', updateMap)
|
||||
adapter.on('updateWaprs', updateWarps)
|
||||
|
||||
return () => {
|
||||
adapter.off('updateMap', updateMap)
|
||||
adapter.off('updateWaprs', updateWarps)
|
||||
}
|
||||
}, [adapter])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// if (drawerRef.current) drawerRef.current.adapter.off('chunkReady', updateChunkOnMap)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const displayFullmap = fullMap && displayMode !== 'minimapOnly' && (showFullmap === 'singleplayer' && singleplayer || showFullmap === 'always')
|
||||
const displayMini = displayMode !== 'fullmapOnly' && (showMinimap === 'singleplayer' && singleplayer || showMinimap === 'always')
|
||||
return displayFullmap
|
||||
? <Fullmap
|
||||
toggleFullMap={() => {
|
||||
toggleFullMap?.({ command: 'ui.toggleMap' })
|
||||
}}
|
||||
adapter={adapter}
|
||||
drawer={drawerRef.current}
|
||||
canvasRef={canvasRef}
|
||||
/>
|
||||
: displayMini
|
||||
? <div
|
||||
className='minimap'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '0px',
|
||||
top: '0px',
|
||||
padding: '5px 5px 0px 0px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
onClick={() => {
|
||||
toggleFullMap?.({ command: 'ui.toggleMap' })
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
style={{
|
||||
transition: '0.5s',
|
||||
transitionTimingFunction: 'ease-out',
|
||||
borderRadius: '1000px'
|
||||
}}
|
||||
width={80}
|
||||
height={80}
|
||||
ref={canvasRef}
|
||||
/>
|
||||
<canvas
|
||||
style={{
|
||||
transition: '0.5s',
|
||||
transitionTimingFunction: 'ease-out',
|
||||
position: 'absolute',
|
||||
left: '0px'
|
||||
}}
|
||||
width={80}
|
||||
height={80}
|
||||
ref={warpsAndPartsCanvasRef}
|
||||
/>
|
||||
<canvas
|
||||
style={{
|
||||
transition: '0.5s',
|
||||
transitionTimingFunction: 'ease-out',
|
||||
position: 'absolute',
|
||||
left: '0px'
|
||||
}}
|
||||
width={80}
|
||||
height={80}
|
||||
ref={playerPosCanvasRef}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.5em',
|
||||
textShadow: '0.1em 0 black, 0 0.1em black, -0.1em 0 black, 0 -0.1em black, -0.1em -0.1em black, -0.1em 0.1em black, 0.1em -0.1em black, 0.1em 0.1em black'
|
||||
}}
|
||||
>
|
||||
{position.x.toFixed(2)} {position.y.toFixed(2)} {position.z.toFixed(2)}
|
||||
</div>
|
||||
</div> : null
|
||||
}
|
||||
324
src/react/MinimapDrawer.ts
Normal file
324
src/react/MinimapDrawer.ts
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import { TypedEventEmitter } from 'contro-max/build/typedEventEmitter'
|
||||
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import { Chunk } from 'prismarine-world/types/world'
|
||||
|
||||
export type MapUpdates = {
|
||||
updateBlockColor: (pos: Vec3) => void
|
||||
updatePlayerPosition: () => void
|
||||
updateWarps: () => void
|
||||
}
|
||||
|
||||
export interface DrawerAdapter extends TypedEventEmitter<MapUpdates> {
|
||||
getHighestBlockY: (x: number, z: number, chunk?: Chunk) => number
|
||||
clearChunksStore: (x: number, z: number) => void
|
||||
chunksStore: Map<string, undefined | null | 'requested' | ChunkInfo >
|
||||
playerPosition: Vec3
|
||||
warps: WorldWarp[]
|
||||
loadingChunksQueue: Set<string>
|
||||
mapDrawer: MinimapDrawer
|
||||
yaw: number
|
||||
full: boolean
|
||||
world: string
|
||||
setWarp: (warp: WorldWarp, remove?: boolean) => void
|
||||
quickTp?: (x: number, z: number) => void
|
||||
loadChunk: (key: string) => Promise<void>
|
||||
drawChunkOnCanvas: (key: string, canvas: HTMLCanvasElement) => Promise<void>
|
||||
}
|
||||
|
||||
export type ChunkInfo = {
|
||||
heightmap: Uint8Array,
|
||||
colors: string[],
|
||||
}
|
||||
|
||||
export class MinimapDrawer {
|
||||
canvasWidthCenterX: number
|
||||
canvasWidthCenterY: number
|
||||
_mapSize: number
|
||||
radius: number
|
||||
ctx: CanvasRenderingContext2D
|
||||
_canvas: HTMLCanvasElement
|
||||
chunksInView = new Set<string>()
|
||||
lastBotPos: Vec3
|
||||
lastWarpPos: Vec3
|
||||
mapPixel: number
|
||||
yaw: number
|
||||
chunksStore = new Map<string, undefined | null | 'requested' | ChunkInfo >()
|
||||
loadingChunksQueue: undefined | Set<string>
|
||||
warps: WorldWarp[]
|
||||
loadChunk: undefined | ((key: string) => Promise<void>)
|
||||
_full = false
|
||||
|
||||
setMapPixel () {
|
||||
if (this.full) {
|
||||
this.radius = Math.floor(Math.min(this.canvas.width, this.canvas.height) / 2)
|
||||
this._mapSize = 16
|
||||
} else {
|
||||
this.radius = Math.floor(Math.min(this.canvas.width, this.canvas.height) / 2.2)
|
||||
this._mapSize = this.radius * 2
|
||||
}
|
||||
this.mapPixel = Math.floor(this.radius * 2 / this.mapSize)
|
||||
}
|
||||
|
||||
get full () {
|
||||
return this._full
|
||||
}
|
||||
|
||||
set full (full: boolean) {
|
||||
this._full = full
|
||||
this.setMapPixel()
|
||||
}
|
||||
|
||||
get canvas () {
|
||||
return this._canvas
|
||||
}
|
||||
|
||||
set canvas (canvas: HTMLCanvasElement) {
|
||||
this.ctx = canvas.getContext('2d', { willReadFrequently: true })!
|
||||
this.ctx.imageSmoothingEnabled = false
|
||||
this.canvasWidthCenterX = canvas.width / 2
|
||||
this.canvasWidthCenterY = canvas.height / 2
|
||||
this._canvas = canvas
|
||||
this.setMapPixel()
|
||||
}
|
||||
|
||||
get mapSize () {
|
||||
return this._mapSize
|
||||
}
|
||||
|
||||
set mapSize (mapSize: number) {
|
||||
this._mapSize = mapSize
|
||||
this.mapPixel = Math.floor(this.radius * 2 / this.mapSize)
|
||||
this.draw(this.lastBotPos)
|
||||
}
|
||||
|
||||
draw (botPos: Vec3,) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
|
||||
this.lastBotPos = botPos
|
||||
this.updateChunksInView()
|
||||
for (const key of this.chunksInView) {
|
||||
if (!this.chunksStore.has(key) && !this.loadingChunksQueue?.has(key)) {
|
||||
void this.loadChunk?.(key)
|
||||
}
|
||||
this.drawChunk(key)
|
||||
}
|
||||
if (!this.full) this.drawPartsOfWorld()
|
||||
}
|
||||
|
||||
updateChunksInView (viewX?: number, viewZ?: number) {
|
||||
const worldCenterX = viewX ?? this.lastBotPos.x
|
||||
const worldCenterZ = viewZ ?? this.lastBotPos.z
|
||||
|
||||
const radius = this.mapSize / 2
|
||||
const leftViewBorder = Math.floor((worldCenterX - radius) / 16) - 1
|
||||
const rightViewBorder = Math.ceil((worldCenterX + radius) / 16)
|
||||
const topViewBorder = Math.floor((worldCenterZ - radius) / 16) - 1
|
||||
const bottomViewBorder = Math.ceil((worldCenterZ + radius) / 16)
|
||||
|
||||
this.chunksInView.clear()
|
||||
for (let i = topViewBorder; i <= bottomViewBorder; i += 1) {
|
||||
for (let j = leftViewBorder; j <= rightViewBorder; j += 1) {
|
||||
this.chunksInView.add(`${j},${i}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawChunk (key: string, chunkInfo?: ChunkInfo | null) {
|
||||
const [chunkX, chunkZ] = key.split(',').map(Number)
|
||||
const chunkWorldX = chunkX * 16
|
||||
const chunkWorldZ = chunkZ * 16
|
||||
const chunkCanvasX = Math.floor((chunkWorldX - this.lastBotPos.x) * this.mapPixel + this.canvasWidthCenterX)
|
||||
const chunkCanvasY = Math.floor((chunkWorldZ - this.lastBotPos.z) * this.mapPixel + this.canvasWidthCenterY)
|
||||
const chunk = chunkInfo ?? this.chunksStore.get(key)
|
||||
if (typeof chunk !== 'object') {
|
||||
const chunkSize = this.mapPixel * 16
|
||||
this.ctx.fillStyle = chunk === 'requested' ? 'rgb(200, 200, 200)' : 'rgba(0, 0, 0, 0.5)'
|
||||
this.ctx.fillRect(chunkCanvasX, chunkCanvasY, chunkSize, chunkSize)
|
||||
return
|
||||
}
|
||||
for (let row = 0; row < 16; row += 1) {
|
||||
for (let col = 0; col < 16; col += 1) {
|
||||
const index = row * 16 + col
|
||||
const color = chunk?.colors[index] ?? 'rgb(255, 0, 0)'
|
||||
const pixelX = chunkCanvasX + this.mapPixel * col
|
||||
const pixelY = chunkCanvasY + this.mapPixel * row
|
||||
this.drawPixel(pixelX, pixelY, color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawPixel (pixelX: number, pixelY: number, color: string) {
|
||||
// if (!this.full && Math.hypot(pixelX - this.canvasWidthCenterX, pixelY - this.canvasWidthCenterY) > this.radius) {
|
||||
// this.ctx.clearRect(pixelX, pixelY, this.mapPixel, this.mapPixel)
|
||||
// return
|
||||
// }
|
||||
this.ctx.fillStyle = color
|
||||
this.ctx.fillRect(
|
||||
pixelX,
|
||||
pixelY,
|
||||
this.mapPixel,
|
||||
this.mapPixel
|
||||
)
|
||||
}
|
||||
|
||||
clearChunksStore () {
|
||||
for (const key of this.chunksStore.keys()) {
|
||||
const [x, z] = key.split(',').map(x => Number(x) * 16)
|
||||
if (Math.hypot((this.lastBotPos.x - x), (this.lastBotPos.z - z)) > this.radius * 5) {
|
||||
this.chunksStore.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setWarpPosOnClick (mousePos: Vec3) {
|
||||
this.lastWarpPos = new Vec3(mousePos.x, mousePos.y, mousePos.z)
|
||||
}
|
||||
|
||||
drawWarps (centerPos?: Vec3) {
|
||||
for (const warp of this.warps) {
|
||||
// if (!full) {
|
||||
// const distance = this.getDistance(
|
||||
// centerPos?.x ?? this.adapter.playerPosition.x,
|
||||
// centerPos?.z ?? this.adapter.playerPosition.z,
|
||||
// warp.x,
|
||||
// warp.z
|
||||
// )
|
||||
// if (distance > this.mapSize) continue
|
||||
// }
|
||||
const offset = this.full ? 0 : this.radius * 0.1
|
||||
const z = Math.floor(
|
||||
(this.mapSize / 2 - (centerPos?.z ?? this.lastBotPos.z) + warp.z) * this.mapPixel
|
||||
) + offset
|
||||
const x = Math.floor(
|
||||
(this.mapSize / 2 - (centerPos?.x ?? this.lastBotPos.x) + warp.x) * this.mapPixel
|
||||
) + offset
|
||||
const dz = z - this.canvasWidthCenterX
|
||||
const dx = x - this.canvasWidthCenterY
|
||||
const circleDist = Math.hypot(dx, dz)
|
||||
|
||||
const angle = Math.atan2(dz, dx)
|
||||
const circleZ = circleDist > this.mapSize / 2 && !this.full ?
|
||||
this.canvasWidthCenterX + this.mapSize / 2 * Math.sin(angle)
|
||||
: z
|
||||
const circleX = circleDist > this.mapSize / 2 && !this.full ?
|
||||
this.canvasWidthCenterY + this.mapSize / 2 * Math.cos(angle)
|
||||
: x
|
||||
this.ctx.beginPath()
|
||||
this.ctx.arc(
|
||||
circleX,
|
||||
circleZ,
|
||||
circleDist > this.mapSize / 2 && !this.full
|
||||
? this.mapPixel * 1.5
|
||||
: this.full ? this.mapPixel : this.mapPixel * 2,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
false
|
||||
)
|
||||
this.ctx.strokeStyle = 'black'
|
||||
this.ctx.lineWidth = this.mapPixel
|
||||
this.ctx.stroke()
|
||||
this.ctx.fillStyle = warp.disabled ? 'rgba(255, 255, 255, 0.4)' : warp.color ?? '#d3d3d3'
|
||||
this.ctx.fill()
|
||||
this.ctx.closePath()
|
||||
}
|
||||
}
|
||||
|
||||
drawPartsOfWorld () {
|
||||
this.ctx.fillStyle = 'white'
|
||||
this.ctx.shadowOffsetX = 1
|
||||
this.ctx.shadowOffsetY = 1
|
||||
this.ctx.shadowColor = 'black'
|
||||
this.ctx.font = `${this.radius / 4}px serif`
|
||||
this.ctx.textAlign = 'center'
|
||||
this.ctx.textBaseline = 'middle'
|
||||
this.ctx.strokeStyle = 'black'
|
||||
this.ctx.lineWidth = 1
|
||||
|
||||
const angle = - Math.PI / 2
|
||||
const angleS = angle + Math.PI
|
||||
const angleW = angle + Math.PI * 3 / 2
|
||||
const angleE = angle + Math.PI / 2
|
||||
|
||||
this.ctx.strokeText(
|
||||
'N',
|
||||
this.canvasWidthCenterX + this.radius * Math.cos(angle),
|
||||
this.canvasWidthCenterY + this.radius * Math.sin(angle)
|
||||
)
|
||||
this.ctx.strokeText(
|
||||
'S',
|
||||
this.canvasWidthCenterX + this.radius * Math.cos(angleS),
|
||||
this.canvasWidthCenterY + this.radius * Math.sin(angleS)
|
||||
)
|
||||
this.ctx.strokeText(
|
||||
'W',
|
||||
this.canvasWidthCenterX + this.radius * Math.cos(angleW),
|
||||
this.canvasWidthCenterY + this.radius * Math.sin(angleW)
|
||||
)
|
||||
this.ctx.strokeText(
|
||||
'E',
|
||||
this.canvasWidthCenterX + this.radius * Math.cos(angleE),
|
||||
this.canvasWidthCenterY + this.radius * Math.sin(angleE)
|
||||
)
|
||||
this.ctx.fillText(
|
||||
'N',
|
||||
this.canvasWidthCenterX + this.radius * Math.cos(angle),
|
||||
this.canvasWidthCenterY + this.radius * Math.sin(angle)
|
||||
)
|
||||
this.ctx.fillText(
|
||||
'S',
|
||||
this.canvasWidthCenterX + this.radius * Math.cos(angleS),
|
||||
this.canvasWidthCenterY + this.radius * Math.sin(angleS)
|
||||
)
|
||||
this.ctx.fillText(
|
||||
'W',
|
||||
this.canvasWidthCenterX + this.radius * Math.cos(angleW),
|
||||
this.canvasWidthCenterY + this.radius * Math.sin(angleW)
|
||||
)
|
||||
this.ctx.fillText(
|
||||
'E',
|
||||
this.canvasWidthCenterX + this.radius * Math.cos(angleE),
|
||||
this.canvasWidthCenterY + this.radius * Math.sin(angleE)
|
||||
)
|
||||
|
||||
this.ctx.shadowOffsetX = 0
|
||||
this.ctx.shadowOffsetY = 0
|
||||
}
|
||||
|
||||
drawPlayerPos (canvasWorldCenterX?: number, canvasWorldCenterZ?: number, disableTurn?: boolean) {
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||
|
||||
const x = (this.lastBotPos.x - (canvasWorldCenterX ?? this.lastBotPos.x)) * this.mapPixel
|
||||
const z = (this.lastBotPos.z - (canvasWorldCenterZ ?? this.lastBotPos.z)) * this.mapPixel
|
||||
const center = this.mapSize / 2 * this.mapPixel + (this.full ? 0 : this.radius * 0.1)
|
||||
this.ctx.translate(center + x, center + z)
|
||||
if (!disableTurn) this.ctx.rotate(-this.yaw)
|
||||
|
||||
const size = 3
|
||||
const factor = this.full ? 2 : 1
|
||||
const width = size * factor
|
||||
const height = size * factor
|
||||
|
||||
this.ctx.beginPath()
|
||||
this.ctx.moveTo(0, -height)
|
||||
this.ctx.lineTo(-width, height)
|
||||
this.ctx.lineTo(width, height)
|
||||
this.ctx.closePath()
|
||||
|
||||
this.ctx.strokeStyle = '#000000'
|
||||
this.ctx.lineWidth = this.full ? 2 : 1
|
||||
this.ctx.stroke()
|
||||
this.ctx.fillStyle = '#FFFFFF'
|
||||
this.ctx.fill()
|
||||
|
||||
// Reset transformations
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||
}
|
||||
|
||||
rotateMap (angle: number) {
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||
this.ctx.translate(this.canvasWidthCenterX, this.canvasWidthCenterY)
|
||||
this.ctx.rotate(angle)
|
||||
this.ctx.translate(-this.canvasWidthCenterX, -this.canvasWidthCenterY)
|
||||
}
|
||||
}
|
||||
610
src/react/MinimapProvider.tsx
Normal file
610
src/react/MinimapProvider.tsx
Normal file
|
|
@ -0,0 +1,610 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { versions } from 'minecraft-data'
|
||||
import { simplify } from 'prismarine-nbt'
|
||||
import RegionFile from 'prismarine-provider-anvil/src/region'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
|
||||
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import { TypedEventEmitter } from 'contro-max/build/typedEventEmitter'
|
||||
import { PCChunk } from 'prismarine-chunk'
|
||||
import { Chunk } from 'prismarine-world/types/world'
|
||||
import { Block } from 'prismarine-block'
|
||||
import { INVISIBLE_BLOCKS } from 'prismarine-viewer/viewer/lib/mesher/worldConstants'
|
||||
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import BlockData from '../../prismarine-viewer/viewer/lib/moreBlockDataGenerated.json'
|
||||
import preflatMap from '../preflatMap.json'
|
||||
import { contro } from '../controls'
|
||||
import { gameAdditionalState, showModal, hideModal, miscUiState, loadedGameState, activeModalStack } from '../globalState'
|
||||
import { options } from '../optionsStorage'
|
||||
import Minimap, { DisplayMode } from './Minimap'
|
||||
import { ChunkInfo, DrawerAdapter, MapUpdates, MinimapDrawer } from './MinimapDrawer'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
|
||||
const getBlockKey = (x: number, z: number) => {
|
||||
return `${x},${z}`
|
||||
}
|
||||
|
||||
const findHeightMap = (obj: PCChunk): number[] | undefined => {
|
||||
function search (obj: any): any | undefined {
|
||||
for (const key in obj) {
|
||||
if (['heightmap', 'heightmaps'].includes(key.toLowerCase())) {
|
||||
return obj[key]
|
||||
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
const result = search(obj[key])
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return search(obj)
|
||||
}
|
||||
|
||||
export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements DrawerAdapter {
|
||||
playerPosition: Vec3
|
||||
yaw: number
|
||||
mapDrawer = new MinimapDrawer()
|
||||
warps: WorldWarp[]
|
||||
world: string
|
||||
chunksStore = new Map<string, undefined | null | 'requested' | ChunkInfo >()
|
||||
loadingChunksQueue = new Set<string>()
|
||||
currChunk: PCChunk | undefined
|
||||
currChunkPos: { x: number, z: number } = { x: 0, z: 0 }
|
||||
isOldVersion: boolean
|
||||
blockData: any
|
||||
heightMap: Record<string, number> = {}
|
||||
regions = new Map<string, RegionFile>()
|
||||
chunksHeightmaps: Record<string, any> = {}
|
||||
loadChunk: (key: string) => Promise<void>
|
||||
loadChunkFullmap: ((key: string) => Promise<ChunkInfo | null | undefined>) | undefined
|
||||
_full: boolean
|
||||
isBuiltinHeightmapAvailable = false
|
||||
|
||||
constructor (pos?: Vec3) {
|
||||
super()
|
||||
this.full = false
|
||||
this.playerPosition = pos ?? new Vec3(0, 0, 0)
|
||||
this.warps = gameAdditionalState.warps
|
||||
this.mapDrawer.warps = this.warps
|
||||
this.mapDrawer.loadChunk = this.loadChunk
|
||||
this.mapDrawer.loadingChunksQueue = this.loadingChunksQueue
|
||||
this.mapDrawer.chunksStore = this.chunksStore
|
||||
|
||||
// check if should use heightmap
|
||||
if (localServer) {
|
||||
const chunkX = Math.floor(this.playerPosition.x / 16)
|
||||
const chunkZ = Math.floor(this.playerPosition.z / 16)
|
||||
const regionX = Math.floor(chunkX / 32)
|
||||
const regionZ = Math.floor(chunkZ / 32)
|
||||
const regionKey = `${regionX},${regionZ}`
|
||||
const worldFolder = this.getSingleplayerRootPath()
|
||||
if (worldFolder && options.minimapOptimizations) {
|
||||
const path = `${worldFolder}/region/r.${regionX}.${regionZ}.mca`
|
||||
const region = new RegionFile(path)
|
||||
void region.initialize()
|
||||
this.regions.set(regionKey, region)
|
||||
const readX = chunkX % 32
|
||||
const readZ = chunkZ % 32
|
||||
void this.regions.get(regionKey)!.read(readX, readZ).then((rawChunk) => {
|
||||
const chunk = simplify(rawChunk as any)
|
||||
const heightmap = findHeightMap(chunk)
|
||||
if (heightmap) {
|
||||
this.isBuiltinHeightmapAvailable = true
|
||||
this.loadChunkFullmap = this.loadChunkFromRegion
|
||||
console.log('using heightmap')
|
||||
} else {
|
||||
this.isBuiltinHeightmapAvailable = false
|
||||
this.loadChunkFullmap = this.loadChunkNoRegion
|
||||
console.log('[minimap] not using heightmap')
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
this.isBuiltinHeightmapAvailable = false
|
||||
this.loadChunkFullmap = this.loadChunkFromViewer
|
||||
})
|
||||
} else {
|
||||
this.isBuiltinHeightmapAvailable = false
|
||||
this.loadChunkFullmap = this.loadChunkFromViewer
|
||||
}
|
||||
} else {
|
||||
this.isBuiltinHeightmapAvailable = false
|
||||
this.loadChunkFullmap = this.loadChunkFromViewer
|
||||
}
|
||||
// if (localServer) {
|
||||
// this.overwriteWarps(localServer.warps)
|
||||
// this.on('cellReady', (key: string) => {
|
||||
// if (this.loadingChunksQueue.size === 0) return
|
||||
// const [x, z] = this.loadingChunksQueue.values().next().value.split(',').map(Number)
|
||||
// this.loadChunk(x, z)
|
||||
// this.loadingChunksQueue.delete(`${x},${z}`)
|
||||
// })
|
||||
// } else {
|
||||
// const storageWarps = localStorage.getItem(`warps: ${loadedGameState.username} ${loadedGameState.serverIp ?? ''}`)
|
||||
// this.overwriteWarps(JSON.parse(storageWarps ?? '[]'))
|
||||
// }
|
||||
this.isOldVersion = versionToNumber(bot.version) < versionToNumber('1.13')
|
||||
this.blockData = {}
|
||||
for (const blockKey of Object.keys(BlockData.colors)) {
|
||||
const renamedKey = getRenamedData('blocks', blockKey, '1.20.2', bot.version)
|
||||
this.blockData[renamedKey as string] = BlockData.colors[blockKey]
|
||||
}
|
||||
|
||||
viewer.world?.renderUpdateEmitter.on('chunkFinished', (key) => {
|
||||
if (!this.loadingChunksQueue.has(key)) return
|
||||
void this.loadChunk(key)
|
||||
this.loadingChunksQueue.delete(key)
|
||||
})
|
||||
}
|
||||
|
||||
get full () {
|
||||
return this._full
|
||||
}
|
||||
|
||||
set full (full: boolean) {
|
||||
console.log('this is minimap')
|
||||
this.loadChunk = this.loadChunkMinimap
|
||||
this.mapDrawer.loadChunk = this.loadChunk
|
||||
this._full = full
|
||||
}
|
||||
|
||||
overwriteWarps (newWarps: WorldWarp[]) {
|
||||
this.warps.splice(0, this.warps.length)
|
||||
for (const warp of newWarps) {
|
||||
this.warps.push({ ...warp })
|
||||
}
|
||||
}
|
||||
|
||||
setWarp (warp: WorldWarp, remove?: boolean): void {
|
||||
this.world = bot.game.dimension
|
||||
const index = this.warps.findIndex(w => w.name === warp.name)
|
||||
if (index === -1) {
|
||||
this.warps.push(warp)
|
||||
} else if (remove && index !== -1) {
|
||||
this.warps.splice(index, 1)
|
||||
} else {
|
||||
this.warps[index] = warp
|
||||
}
|
||||
if (localServer) {
|
||||
// type suppressed until server is updated. It works fine
|
||||
void (localServer as any).setWarp(warp, remove)
|
||||
} else if (remove) {
|
||||
localStorage.removeItem(`warps: ${loadedGameState.username} ${loadedGameState.serverIp}`)
|
||||
} else {
|
||||
localStorage.setItem(`warps: ${loadedGameState.username} ${loadedGameState.serverIp}`, JSON.stringify(this.warps))
|
||||
}
|
||||
this.emit('updateWarps')
|
||||
}
|
||||
|
||||
getHighestBlockY (x: number, z: number, chunk?: Chunk) {
|
||||
const chunkX = Math.floor(x / 16) * 16
|
||||
const chunkZ = Math.floor(z / 16) * 16
|
||||
if (this.chunksHeightmaps[`${chunkX},${chunkZ}`]) {
|
||||
return this.chunksHeightmaps[`${chunkX},${chunkZ}`][x - chunkX + (z - chunkZ) * 16] - 1
|
||||
}
|
||||
const source = chunk ?? bot.world
|
||||
const { height, minY } = (bot.game as any)
|
||||
for (let i = height; i > 0; i -= 1) {
|
||||
const block = source.getBlock(new Vec3(x & 15, minY + i, z & 15))
|
||||
if (block && !INVISIBLE_BLOCKS.has(block.name)) {
|
||||
return minY + i
|
||||
}
|
||||
}
|
||||
return minY
|
||||
}
|
||||
|
||||
async getChunkSingleplayer (chunkX: number, chunkZ: number) {
|
||||
// absolute coords
|
||||
const region = (localServer!.overworld.storageProvider as any).getRegion(chunkX * 16, chunkZ * 16)
|
||||
if (!region) return 'unavailable'
|
||||
const chunk = await localServer!.players[0]!.world.getColumn(chunkX, chunkZ)
|
||||
return chunk
|
||||
}
|
||||
|
||||
async loadChunkMinimap (key: string) {
|
||||
const [chunkX, chunkZ] = key.split(',').map(Number)
|
||||
const chunkWorldX = chunkX * 16
|
||||
const chunkWorldZ = chunkZ * 16
|
||||
if (viewer.world.finishedChunks[`${chunkWorldX},${chunkWorldZ}`]) {
|
||||
const heightmap = new Uint8Array(256)
|
||||
const colors = Array.from({ length: 256 }).fill('') as string[]
|
||||
for (let z = 0; z < 16; z += 1) {
|
||||
for (let x = 0; x < 16; x += 1) {
|
||||
const blockX = chunkWorldX + x
|
||||
const blockZ = chunkWorldZ + z
|
||||
const hBlock = viewer.world.highestBlocks.get(`${blockX},${blockZ}`)
|
||||
const block = bot.world.getBlock(new Vec3(blockX, hBlock?.y ?? 0, blockZ))
|
||||
// const block = Block.fromStateId(hBlock?.stateId ?? -1, hBlock?.biomeId ?? -1)
|
||||
const index = z * 16 + x
|
||||
if (!block || !hBlock) {
|
||||
console.warn(`[loadChunk] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`)
|
||||
heightmap[index] = 0
|
||||
colors[index] = 'rgba(0, 0, 0, 0.5)'
|
||||
continue
|
||||
}
|
||||
heightmap[index] = hBlock.y
|
||||
let color: string
|
||||
if (this.isOldVersion) {
|
||||
color = BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')]
|
||||
?? 'rgb(0, 0, 255)'
|
||||
} else {
|
||||
color = this.blockData[block.name] ?? 'rgb(0, 255, 0)'
|
||||
}
|
||||
colors[index] = color
|
||||
}
|
||||
}
|
||||
const chunk = { heightmap, colors }
|
||||
this.applyShadows(chunk)
|
||||
this.chunksStore.set(key, chunk)
|
||||
this.emit(`chunkReady`, `${chunkX},${chunkZ}`)
|
||||
} else {
|
||||
this.loadingChunksQueue.add(`${chunkX},${chunkZ}`)
|
||||
this.chunksStore.set(key, 'requested')
|
||||
}
|
||||
}
|
||||
|
||||
async loadChunkNoRegion (key: string) {
|
||||
const [chunkX, chunkZ] = key.split(',').map(Number)
|
||||
const chunkWorldX = chunkX * 16
|
||||
const chunkWorldZ = chunkZ * 16
|
||||
const chunkInfo = await this.getChunkSingleplayer(chunkX, chunkZ)
|
||||
if (chunkInfo === 'unavailable') return null
|
||||
const heightmap = new Uint8Array(256)
|
||||
const colors = Array.from({ length: 256 }).fill('') as string[]
|
||||
for (let z = 0; z < 16; z += 1) {
|
||||
for (let x = 0; x < 16; x += 1) {
|
||||
const blockX = chunkWorldX + x
|
||||
const blockZ = chunkWorldZ + z
|
||||
const blockY = this.getHighestBlockY(blockX, blockZ, chunkInfo)
|
||||
const block = chunkInfo.getBlock(new Vec3(blockX & 15, blockY, blockZ & 15))
|
||||
if (!block) {
|
||||
console.warn(`[cannot get the block] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`)
|
||||
return null
|
||||
}
|
||||
const index = z * 16 + x
|
||||
heightmap[index] = blockY
|
||||
const color = this.isOldVersion ? BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] ?? 'rgb(0, 0, 255)' : this.blockData[block.name] ?? 'rgb(0, 255, 0)'
|
||||
colors[index] = color
|
||||
}
|
||||
}
|
||||
const chunk: ChunkInfo = { heightmap, colors }
|
||||
this.applyShadows(chunk)
|
||||
return chunk
|
||||
}
|
||||
|
||||
async loadChunkFromRegion (key: string): Promise<ChunkInfo | null | undefined> {
|
||||
const [chunkX, chunkZ] = key.split(',').map(Number)
|
||||
const chunkWorldX = chunkX * 16
|
||||
const chunkWorldZ = chunkZ * 16
|
||||
const heightmap = await this.getChunkHeightMapFromRegion(chunkX, chunkZ) as unknown as Uint8Array
|
||||
if (!heightmap) return null
|
||||
const chunkInfo = await this.getChunkSingleplayer(chunkX, chunkZ)
|
||||
if (chunkInfo === 'unavailable') return null
|
||||
const colors = Array.from({ length: 256 }).fill('') as string[]
|
||||
for (let z = 0; z < 16; z += 1) {
|
||||
for (let x = 0; x < 16; x += 1) {
|
||||
const blockX = chunkWorldX + x
|
||||
const blockZ = chunkWorldZ + z
|
||||
const index = z * 16 + x
|
||||
heightmap[index] -= 1
|
||||
if (heightmap[index] < 0) heightmap[index] = 0
|
||||
const blockY = heightmap[index]
|
||||
const block = chunkInfo.getBlock(new Vec3(blockX & 15, blockY, blockZ & 15))
|
||||
if (!block) {
|
||||
console.warn(`[cannot get the block] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`)
|
||||
return null
|
||||
}
|
||||
const color = this.isOldVersion ? BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] ?? 'rgb(0, 0, 255)' : this.blockData[block.name] ?? 'rgb(0, 255, 0)'
|
||||
colors[index] = color
|
||||
}
|
||||
}
|
||||
const chunk: ChunkInfo = { heightmap, colors }
|
||||
this.applyShadows(chunk)
|
||||
return chunk
|
||||
}
|
||||
|
||||
getSingleplayerRootPath (): string | undefined {
|
||||
return localServer!.options.worldFolder
|
||||
}
|
||||
|
||||
async getChunkHeightMapFromRegion (chunkX: number, chunkZ: number, cb?: (hm: number[]) => void) {
|
||||
const regionX = Math.floor(chunkX / 32)
|
||||
const regionZ = Math.floor(chunkZ / 32)
|
||||
const regionKey = `${regionX},${regionZ}`
|
||||
if (!this.regions.has(regionKey)) {
|
||||
const worldFolder = this.getSingleplayerRootPath()
|
||||
if (!worldFolder) return
|
||||
const path = `${worldFolder}/region/r.${regionX}.${regionZ}.mca`
|
||||
const region = new RegionFile(path)
|
||||
await region.initialize()
|
||||
this.regions.set(regionKey, region)
|
||||
}
|
||||
const rawChunk = await this.regions.get(regionKey)!.read(chunkX % 32, chunkZ % 32)
|
||||
const chunk = simplify(rawChunk as any)
|
||||
console.log(`chunk ${chunkX}, ${chunkZ}:`, chunk)
|
||||
const heightmap = findHeightMap(chunk)
|
||||
console.log(`heightmap ${chunkX}, ${chunkZ}:`, heightmap)
|
||||
cb?.(heightmap!)
|
||||
return heightmap
|
||||
// this.chunksHeightmaps[`${chunkX},${chunkZ}`] = heightmap
|
||||
}
|
||||
|
||||
async loadChunkFromViewer (key: string) {
|
||||
const [chunkX, chunkZ] = key.split(',').map(Number)
|
||||
const chunkWorldX = chunkX * 16
|
||||
const chunkWorldZ = chunkZ * 16
|
||||
if (viewer.world.finishedChunks[`${chunkWorldX},${chunkWorldZ}`]) {
|
||||
const heightmap = new Uint8Array(256)
|
||||
const colors = Array.from({ length: 256 }).fill('') as string[]
|
||||
for (let z = 0; z < 16; z += 1) {
|
||||
for (let x = 0; x < 16; x += 1) {
|
||||
const blockX = chunkWorldX + x
|
||||
const blockZ = chunkWorldZ + z
|
||||
const hBlock = viewer.world.highestBlocks.get(`${blockX},${blockZ}`)
|
||||
const block = bot.world.getBlock(new Vec3(blockX, hBlock?.y ?? 0, blockZ))
|
||||
// const block = Block.fromStateId(hBlock?.stateId ?? -1, hBlock?.biomeId ?? -1)
|
||||
const index = z * 16 + x
|
||||
if (!block || !hBlock) {
|
||||
console.warn(`[loadChunk] ${chunkX}, ${chunkZ}, ${chunkWorldX + x}, ${chunkWorldZ + z}`)
|
||||
heightmap[index] = 0
|
||||
colors[index] = 'rgba(0, 0, 0, 0.5)'
|
||||
continue
|
||||
}
|
||||
heightmap[index] = hBlock.y
|
||||
const color = this.isOldVersion ? BlockData.colors[preflatMap.blocks[`${block.type}:${block.metadata}`]?.replaceAll(/\[.*?]/g, '')] ?? 'rgb(0, 0, 255)' : this.blockData[block.name] ?? 'rgb(0, 255, 0)'
|
||||
colors[index] = color
|
||||
}
|
||||
}
|
||||
const chunk = { heightmap, colors }
|
||||
this.applyShadows(chunk)
|
||||
return chunk
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
applyShadows (chunk: ChunkInfo) {
|
||||
for (let j = 0; j < 16; j += 1) {
|
||||
for (let i = 0; i < 16; i += 1) {
|
||||
const index = j * 16 + i
|
||||
const color = chunk.colors[index]
|
||||
// if (i === 0 || j === 0 || i === 15 || j === 16) {
|
||||
// const r = Math.floor(Math.random() * 2)
|
||||
// chunk.colors[index] = r===0 ? this.makeDarker(color) : this.makeLighter(color)
|
||||
// continue
|
||||
// }
|
||||
|
||||
const h = chunk.heightmap[index]
|
||||
let isLighterOrDarker = 0
|
||||
|
||||
const r = chunk.heightmap[index + 1] ?? 0
|
||||
const u = chunk.heightmap[index - 16] ?? 0
|
||||
const ur = chunk.heightmap[index - 15] ?? 0
|
||||
if (r > h || u > h || ur > h) {
|
||||
chunk.colors[index] = this.makeDarker(color)
|
||||
isLighterOrDarker -= 1
|
||||
}
|
||||
|
||||
const l = chunk.heightmap[index - 1] ?? 0
|
||||
const d = chunk.heightmap[index + 16] ?? 0
|
||||
const dl = chunk.heightmap[index + 15] ?? 0
|
||||
if (l > h || d > h || dl > h) {
|
||||
chunk.colors[index] = this.makeLighter(color)
|
||||
isLighterOrDarker += 1
|
||||
}
|
||||
|
||||
let linkedIndex: number | undefined
|
||||
if (i === 1) {
|
||||
linkedIndex = index - 1
|
||||
} else if (i === 14) {
|
||||
linkedIndex = index + 1
|
||||
} else if (j === 1) {
|
||||
linkedIndex = index - 16
|
||||
} else if (j === 14) {
|
||||
linkedIndex = index + 16
|
||||
}
|
||||
if (linkedIndex !== undefined) {
|
||||
const linkedColor = chunk.colors[linkedIndex]
|
||||
switch (isLighterOrDarker) {
|
||||
case 1:
|
||||
chunk.colors[linkedIndex] = this.makeLighter(linkedColor)
|
||||
break
|
||||
case -1:
|
||||
chunk.colors[linkedIndex] = this.makeDarker(linkedColor)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
makeDarker (color: string) {
|
||||
let rgbArray = color.match(/\d+/g)?.map(Number) ?? []
|
||||
if (rgbArray.length !== 3) return color
|
||||
rgbArray = rgbArray.map(element => {
|
||||
let newColor = element - 20
|
||||
if (newColor < 0) newColor = 0
|
||||
return newColor
|
||||
})
|
||||
return `rgb(${rgbArray.join(',')})`
|
||||
}
|
||||
|
||||
makeLighter (color: string) {
|
||||
let rgbArray = color.match(/\d+/g)?.map(Number) ?? []
|
||||
if (rgbArray.length !== 3) return color
|
||||
rgbArray = rgbArray.map(element => {
|
||||
let newColor = element + 20
|
||||
if (newColor > 255) newColor = 255
|
||||
return newColor
|
||||
})
|
||||
return `rgb(${rgbArray.join(',')})`
|
||||
}
|
||||
|
||||
clearChunksStore (x: number, z: number) {
|
||||
for (const key of Object.keys(this.chunksStore)) {
|
||||
const [chunkX, chunkZ] = key.split(',').map(Number)
|
||||
if (Math.hypot((chunkX - x), (chunkZ - z)) > 300) {
|
||||
delete this.chunksStore[key]
|
||||
delete this.chunksHeightmaps[key]
|
||||
for (let i = 0; i < 16; i += 1) {
|
||||
for (let j = 0; j < 16; j += 1) {
|
||||
delete this.heightMap[`${chunkX + i},${chunkZ + j}`]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quickTp (x: number, z: number) {
|
||||
const y = this.getHighestBlockY(x, z)
|
||||
bot.chat(`/tp ${x} ${y + 20} ${z}`)
|
||||
const timeout = setTimeout(() => {
|
||||
const y = this.getHighestBlockY(x, z)
|
||||
bot.chat(`/tp ${x} ${y + 20} ${z}`)
|
||||
clearTimeout(timeout)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async drawChunkOnCanvas (key: string, canvas: HTMLCanvasElement) {
|
||||
// console.log('chunk', key, 'on canvas')
|
||||
if (!this.loadChunkFullmap) {
|
||||
// wait for it to be available
|
||||
await new Promise(resolve => {
|
||||
const interval = setInterval(() => {
|
||||
if (this.loadChunkFullmap) {
|
||||
clearInterval(interval)
|
||||
resolve(undefined)
|
||||
}
|
||||
}, 100)
|
||||
setTimeout(() => {
|
||||
clearInterval(interval)
|
||||
resolve(undefined)
|
||||
}, 10_000)
|
||||
})
|
||||
if (!this.loadChunkFullmap) {
|
||||
throw new Error('loadChunkFullmap not available')
|
||||
}
|
||||
}
|
||||
const chunk = await this.loadChunkFullmap(key)
|
||||
const [worldX, worldZ] = key.split(',').map(x => Number(x) * 16)
|
||||
const center = new Vec3(worldX + 8, 0, worldZ + 8)
|
||||
this.mapDrawer.lastBotPos = center
|
||||
this.mapDrawer.canvas = canvas
|
||||
this.mapDrawer.full = true
|
||||
this.mapDrawer.drawChunk(key, chunk)
|
||||
}
|
||||
}
|
||||
|
||||
const Inner = (
|
||||
{ adapter, displayMode, toggleFullMap }:
|
||||
{
|
||||
adapter: DrawerAdapterImpl
|
||||
displayMode?: DisplayMode,
|
||||
toggleFullMap?: ({ command }: { command?: string }) => void
|
||||
}
|
||||
) => {
|
||||
|
||||
const updateWarps = (newWarps: WorldWarp[] | Error) => {
|
||||
if (newWarps instanceof Error) {
|
||||
console.error('An error occurred:', newWarps.message)
|
||||
return
|
||||
}
|
||||
|
||||
adapter.overwriteWarps(newWarps)
|
||||
}
|
||||
|
||||
const updateMap = () => {
|
||||
if (!adapter) return
|
||||
adapter.playerPosition = bot.entity.position
|
||||
adapter.yaw = bot.entity.yaw
|
||||
adapter.emit('updateMap')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
bot.on('move', updateMap)
|
||||
localServer?.on('warpsUpdated' as keyof ServerEvents, updateWarps)
|
||||
|
||||
return () => {
|
||||
bot?.off('move', updateMap)
|
||||
localServer?.off('warpsUpdated' as keyof ServerEvents, updateWarps)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div>
|
||||
<Minimap
|
||||
adapter={adapter}
|
||||
showMinimap={options.showMinimap}
|
||||
showFullmap='always'
|
||||
singleplayer={miscUiState.singleplayer}
|
||||
fullMap={displayMode === 'fullmapOnly'}
|
||||
toggleFullMap={toggleFullMap}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ({ displayMode }: { displayMode?: DisplayMode }) => {
|
||||
const [adapter] = useState(() => new DrawerAdapterImpl(bot.entity.position))
|
||||
|
||||
const { showMinimap } = useSnapshot(options)
|
||||
const fullMapOpened = useIsModalActive('full-map')
|
||||
|
||||
|
||||
const readChunksHeightMaps = async () => {
|
||||
const { worldFolder } = localServer!.options
|
||||
const path = `${worldFolder}/region/r.0.0.mca`
|
||||
const region = new RegionFile(path)
|
||||
await region.initialize()
|
||||
const chunks: Record<string, any> = {}
|
||||
console.log('Reading chunks...')
|
||||
console.log(chunks)
|
||||
let versionDetected = false
|
||||
for (const [i, _] of Array.from({ length: 32 }).entries()) {
|
||||
for (const [k, _] of Array.from({ length: 32 }).entries()) {
|
||||
// todo, may use faster reading, but features is not commonly used
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const nbt = await region.read(i, k)
|
||||
chunks[`${i},${k}`] = nbt
|
||||
if (nbt && !versionDetected) {
|
||||
const simplified = simplify(nbt)
|
||||
const version = versions.pc.find(x => x['dataVersion'] === simplified.DataVersion)?.minecraftVersion
|
||||
console.log('Detected version', version ?? 'unknown')
|
||||
versionDetected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.defineProperty(chunks, 'simplified', {
|
||||
get () {
|
||||
const mapped = {}
|
||||
for (const [i, _] of Array.from({ length: 32 }).entries()) {
|
||||
for (const [k, _] of Array.from({ length: 32 }).entries()) {
|
||||
const key = `${i},${k}`
|
||||
const chunk = chunks[key]
|
||||
if (!chunk) continue
|
||||
mapped[key] = simplify(chunk)
|
||||
}
|
||||
}
|
||||
return mapped
|
||||
},
|
||||
})
|
||||
console.log('Done!', chunks)
|
||||
}
|
||||
|
||||
if (
|
||||
displayMode === 'minimapOnly'
|
||||
? showMinimap === 'never' || (showMinimap === 'singleplayer' && !miscUiState.singleplayer)
|
||||
: !fullMapOpened
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const toggleFullMap = () => {
|
||||
if (activeModalStack.at(-1)?.reactType === 'full-map') {
|
||||
hideModal({ reactType: 'full-map' })
|
||||
} else {
|
||||
showModal({ reactType: 'full-map' })
|
||||
}
|
||||
}
|
||||
|
||||
return <Inner adapter={adapter} displayMode={displayMode} toggleFullMap={toggleFullMap} />
|
||||
}
|
||||
|
|
@ -4,15 +4,16 @@ interface Props {
|
|||
backdrop?: boolean | 'dirt'
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
titleSelectable?: boolean
|
||||
}
|
||||
|
||||
export default ({ title, children, backdrop = true, style, className }: Props) => {
|
||||
export default ({ title, children, backdrop = true, style, className, titleSelectable }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{backdrop === 'dirt' ? <div className='dirt-bg' /> : backdrop ? <div className="backdrop" /> : null}
|
||||
<div className={`fullscreen ${className}`} style={{ overflow: 'auto', ...style }}>
|
||||
<div className="screen-content">
|
||||
<div className="screen-title">{title}</div>
|
||||
<div className={`screen-title ${titleSelectable ? 'text-select' : ''}`}>{title}</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
|
|||
import { useDidUpdateEffect } from './utils'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
import { showOptionsModal } from './SelectOption'
|
||||
import { useCopyKeybinding } from './simpleHooks'
|
||||
|
||||
interface StoreServerItem extends BaseServerInfo {
|
||||
lastJoined?: number
|
||||
|
|
@ -92,7 +93,11 @@ const getInitialServersList = () => {
|
|||
return servers
|
||||
}
|
||||
|
||||
const setNewServersList = (serversList: StoreServerItem[]) => {
|
||||
const serversListQs = new URLSearchParams(window.location.search).get('serversList')
|
||||
const proxyQs = new URLSearchParams(window.location.search).get('proxy')
|
||||
|
||||
const setNewServersList = (serversList: StoreServerItem[], force = false) => {
|
||||
if (serversListQs && !force) return
|
||||
localStorage['serversList'] = JSON.stringify(serversList)
|
||||
|
||||
// cleanup legacy
|
||||
|
|
@ -133,13 +138,14 @@ export const updateAuthenticatedAccountData = (callback: (data: AuthenticatedAcc
|
|||
// todo move to base
|
||||
const normalizeIp = (ip: string) => ip.replace(/https?:\/\//, '').replace(/\/(:|$)/, '')
|
||||
|
||||
const Inner = ({ hidden }: { hidden?: boolean }) => {
|
||||
const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersList?: string[] }) => {
|
||||
const [proxies, setProxies] = useState<readonly string[]>(localStorage['proxies'] ? JSON.parse(localStorage['proxies']) : getInitialProxies())
|
||||
const [selectedProxy, setSelectedProxy] = useState(localStorage.getItem('selectedProxy') ?? proxies?.[0] ?? '')
|
||||
const [selectedProxy, setSelectedProxy] = useState(proxyQs ?? localStorage.getItem('selectedProxy') ?? proxies?.[0] ?? '')
|
||||
const [serverEditScreen, setServerEditScreen] = useState<StoreServerItem | true | null>(null) // true for add
|
||||
const [defaultUsername, _setDefaultUsername] = useState(localStorage['username'] ?? (`mcrafter${Math.floor(Math.random() * 1000)}`))
|
||||
const [authenticatedAccounts, _setAuthenticatedAccounts] = useState<AuthenticatedAccount[]>(JSON.parse(localStorage['authenticatedAccounts'] || '[]'))
|
||||
const [quickConnectIp, setQuickConnectIp] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
|
||||
const setAuthenticatedAccounts = (newState: typeof authenticatedAccounts) => {
|
||||
_setAuthenticatedAccounts(newState)
|
||||
|
|
@ -151,18 +157,35 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
|
|||
localStorage.setItem('username', newState)
|
||||
}
|
||||
|
||||
const saveNewProxy = () => {
|
||||
if (!selectedProxy || proxyQs) return
|
||||
localStorage.setItem('selectedProxy', selectedProxy)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (proxies.length) {
|
||||
localStorage.setItem('proxies', JSON.stringify(proxies))
|
||||
}
|
||||
if (selectedProxy) {
|
||||
localStorage.setItem('selectedProxy', selectedProxy)
|
||||
}
|
||||
saveNewProxy()
|
||||
}, [proxies])
|
||||
|
||||
const [serversList, setServersList] = useState<StoreServerItem[]>(() => getInitialServersList())
|
||||
const [serversList, setServersList] = useState<StoreServerItem[]>(() => (customServersList ? [] : getInitialServersList()))
|
||||
const [additionalData, setAdditionalData] = useState<Record<string, AdditionalDisplayData>>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (customServersList) {
|
||||
setServersList(customServersList.map(row => {
|
||||
const [ip, name] = row.split(' ')
|
||||
const [_ip, _port, version] = ip.split(':')
|
||||
return {
|
||||
ip,
|
||||
versionOverride: version,
|
||||
name,
|
||||
}
|
||||
}))
|
||||
}
|
||||
}, [customServersList])
|
||||
|
||||
useDidUpdateEffect(() => {
|
||||
// save data only on user changes
|
||||
setNewServersList(serversList)
|
||||
|
|
@ -218,6 +241,16 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
|
|||
}
|
||||
}, [isEditScreenModal])
|
||||
|
||||
useCopyKeybinding(() => {
|
||||
const item = serversList[selectedIndex]
|
||||
if (!item) return
|
||||
let str = `${item.ip}`
|
||||
if (item.versionOverride) {
|
||||
str += `:${item.versionOverride}`
|
||||
}
|
||||
return str
|
||||
})
|
||||
|
||||
const editModalJsx = isEditScreenModal ? <AddServerOrConnect
|
||||
placeholders={{
|
||||
proxyOverride: selectedProxy,
|
||||
|
|
@ -319,15 +352,14 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
|
|||
// setProxies([...proxies, selectedProxy])
|
||||
localStorage.setItem('proxies', JSON.stringify([...proxies, selectedProxy]))
|
||||
}
|
||||
if (selectedProxy) {
|
||||
localStorage.setItem('selectedProxy', selectedProxy)
|
||||
}
|
||||
saveNewProxy()
|
||||
},
|
||||
serverIndex: shouldSave ? serversList.length.toString() : indexOrIp // assume last
|
||||
} satisfies ConnectOptions
|
||||
dispatchEvent(new CustomEvent('connect', { detail: options }))
|
||||
// qsOptions
|
||||
}}
|
||||
lockedEditing={!!customServersList}
|
||||
username={defaultUsername}
|
||||
setUsername={setDefaultUsername}
|
||||
setQuickConnectIp={setQuickConnectIp}
|
||||
|
|
@ -377,6 +409,9 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
|
|||
setSelectedProxy(selected)
|
||||
}}
|
||||
hidden={hidden}
|
||||
onRowSelect={(_, i) => {
|
||||
setSelectedIndex(i)
|
||||
}}
|
||||
/>
|
||||
return <>
|
||||
{serversListJsx}
|
||||
|
|
@ -385,6 +420,24 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
|
|||
}
|
||||
|
||||
export default () => {
|
||||
const [customServersList, setCustomServersList] = useState<string[] | undefined>(serversListQs ? [] : undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (serversListQs) {
|
||||
if (serversListQs.startsWith('http')) {
|
||||
void fetch(serversListQs).then(async r => r.text()).then((text) => {
|
||||
const isJson = serversListQs.endsWith('.json') ? true : serversListQs.endsWith('.txt') ? false : text.startsWith('[')
|
||||
setCustomServersList(isJson ? JSON.parse(text) : text.split('\n').map(x => x.trim()).filter(x => x.trim().length > 0))
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
alert(`Failed to get servers list file: ${err}`)
|
||||
})
|
||||
} else {
|
||||
setCustomServersList(serversListQs.split(','))
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const modalStack = useSnapshot(activeModalStack)
|
||||
const hasServersListModal = modalStack.some(x => x.reactType === 'serversList')
|
||||
const editServerModalActive = useIsModalActive('editServer')
|
||||
|
|
@ -392,5 +445,5 @@ export default () => {
|
|||
|
||||
const eitherModal = isServersListModalActive || editServerModalActive
|
||||
const render = eitherModal || hasServersListModal
|
||||
return render ? <Inner hidden={!isServersListModalActive} /> : null
|
||||
return render ? <Inner hidden={!isServersListModalActive} customServersList={customServersList} /> : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ interface Props {
|
|||
listStyle?: React.CSSProperties
|
||||
setListHovered?: (hovered: boolean) => void
|
||||
secondRowStyles?: React.CSSProperties
|
||||
lockedEditing?: boolean
|
||||
}
|
||||
|
||||
export default ({
|
||||
|
|
@ -116,7 +117,8 @@ export default ({
|
|||
defaultSelectedRow,
|
||||
listStyle,
|
||||
setListHovered,
|
||||
secondRowStyles
|
||||
secondRowStyles,
|
||||
lockedEditing
|
||||
}: Props) => {
|
||||
const containerRef = useRef<any>()
|
||||
const firstButton = useRef<HTMLButtonElement>(null)
|
||||
|
|
@ -213,10 +215,10 @@ export default ({
|
|||
<Button onClick={() => onGeneralAction('create')} disabled={isReadonly}>Create New World</Button>
|
||||
</div>}
|
||||
<div style={{ ...secondRowStyles }}>
|
||||
{serversLayout ? <Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('edit', focusedWorld)}>Edit</Button> : <Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('export', focusedWorld)}>Export</Button>}
|
||||
<Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('delete', focusedWorld)}>Delete</Button>
|
||||
{serversLayout ? <Button style={{ width: 100 }} disabled={!focusedWorld || lockedEditing} onClick={() => onWorldAction('edit', focusedWorld)}>Edit</Button> : <Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('export', focusedWorld)}>Export</Button>}
|
||||
<Button style={{ width: 100 }} disabled={!focusedWorld || lockedEditing} onClick={() => onWorldAction('delete', focusedWorld)}>Delete</Button>
|
||||
{serversLayout ?
|
||||
<Button style={{ width: 100 }} onClick={() => onGeneralAction('create')}>Add</Button> :
|
||||
<Button style={{ width: 100 }} onClick={() => onGeneralAction('create')} disabled={lockedEditing}>Add</Button> :
|
||||
<Button style={{ width: 100 }} onClick={() => onWorldAction('edit', focusedWorld)} disabled>Edit</Button>}
|
||||
<Button style={{ width: 100 }} onClick={() => onGeneralAction('cancel')}>Cancel</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
display: block;
|
||||
position: absolute;
|
||||
top: 37px;
|
||||
left: calc(88px + 5px);
|
||||
left: calc((512px / 2 - 176px / 2) / 2);
|
||||
background-image: url('../../assets/edition.png');
|
||||
background-size: 128px;
|
||||
width: 88px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,27 @@
|
|||
import { useUtilsEffect } from '@zardoy/react-util'
|
||||
import { useMedia } from 'react-use'
|
||||
|
||||
const SMALL_SCREEN_MEDIA = '@media (max-width: 440px)'
|
||||
export const useIsSmallWidth = () => {
|
||||
return useMedia(SMALL_SCREEN_MEDIA.replace('@media ', ''))
|
||||
}
|
||||
|
||||
export const useCopyKeybinding = (getCopyText: () => string | undefined) => {
|
||||
useUtilsEffect(({ signal }) => {
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.code === 'KeyC' && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
|
||||
const { activeElement } = document
|
||||
if (activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement) {
|
||||
return
|
||||
}
|
||||
if (window.getSelection()?.toString()) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
const copyText = getCopyText()
|
||||
if (!copyText) return
|
||||
void navigator.clipboard.writeText(copyText)
|
||||
}
|
||||
}, { signal })
|
||||
}, [getCopyText])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ const useLongPress = (
|
|||
)
|
||||
|
||||
const clear = useCallback(
|
||||
(event: React.MouseEvent | React.TouchEvent, shouldTriggerClick = true) => {
|
||||
(event: React.MouseEvent | React.TouchEvent) => {
|
||||
if (timeout.current) clearTimeout(timeout.current)
|
||||
if (shouldTriggerClick && !longPressTriggered) onClick()
|
||||
if (!longPressTriggered) onClick()
|
||||
setLongPressTriggered(false)
|
||||
if (shouldPreventDefault && target.current) {
|
||||
target.current.removeEventListener('touchend', preventDefault)
|
||||
|
|
@ -46,7 +46,7 @@ const useLongPress = (
|
|||
onMouseDown: (e: React.MouseEvent) => start(e),
|
||||
onTouchStart: (e: React.TouchEvent) => start(e),
|
||||
onMouseUp: (e: React.MouseEvent) => clear(e),
|
||||
onMouseLeave: (e: React.MouseEvent) => clear(e, false),
|
||||
onMouseLeave: (e: React.MouseEvent) => clear(e),
|
||||
onTouchEnd: (e: React.TouchEvent) => clear(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -61,4 +61,3 @@ const preventDefault = (event: Event) => {
|
|||
}
|
||||
|
||||
export default useLongPress
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { activeModalStack, miscUiState } from '../globalState'
|
|||
export const watchedModalsFromHooks = new Set<string>()
|
||||
// todo should not be there
|
||||
export const hardcodedKnownModals = [
|
||||
'player_win:'
|
||||
'player_win:',
|
||||
'full-map' // todo
|
||||
]
|
||||
|
||||
export const useUsingTouch = () => {
|
||||
|
|
@ -17,6 +18,7 @@ export const useIsModalActive = (modal: string, useIncludes = false) => {
|
|||
watchedModalsFromHooks.add(modal)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
// watchedModalsFromHooks.add(modal)
|
||||
return () => {
|
||||
watchedModalsFromHooks.delete(modal)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import ScoreboardProvider from './react/ScoreboardProvider'
|
|||
import SignEditorProvider from './react/SignEditorProvider'
|
||||
import IndicatorEffectsProvider from './react/IndicatorEffectsProvider'
|
||||
import PlayerListOverlayProvider from './react/PlayerListOverlayProvider'
|
||||
import MinimapProvider from './react/MinimapProvider'
|
||||
import HudBarsProvider from './react/HudBarsProvider'
|
||||
import XPBarProvider from './react/XPBarProvider'
|
||||
import DebugOverlay from './react/DebugOverlay'
|
||||
|
|
@ -27,7 +28,7 @@ import PauseScreen from './react/PauseScreen'
|
|||
import SoundMuffler from './react/SoundMuffler'
|
||||
import TouchControls from './react/TouchControls'
|
||||
import widgets from './react/widgets'
|
||||
import { useIsWidgetActive } from './react/utilsApp'
|
||||
import { useIsModalActive, useIsWidgetActive } from './react/utilsApp'
|
||||
import GlobalSearchInput from './react/GlobalSearchInput'
|
||||
import TouchAreasControlsProvider from './react/TouchAreasControlsProvider'
|
||||
import NotificationProvider, { showNotification } from './react/NotificationProvider'
|
||||
|
|
@ -101,9 +102,13 @@ const InGameComponent = ({ children }) => {
|
|||
|
||||
const InGameUi = () => {
|
||||
const { gameLoaded, showUI: showUIRaw } = useSnapshot(miscUiState)
|
||||
const { disabledUiParts, displayBossBars } = useSnapshot(options)
|
||||
const hasModals = useSnapshot(activeModalStack).length > 0
|
||||
const { disabledUiParts, displayBossBars, showMinimap } = useSnapshot(options)
|
||||
const modalsSnapshot = useSnapshot(activeModalStack)
|
||||
const hasModals = modalsSnapshot.length > 0
|
||||
const showUI = showUIRaw || hasModals
|
||||
const displayFullmap = modalsSnapshot.some(modal => modal.reactType === 'full-map')
|
||||
// bot can't be used here
|
||||
|
||||
if (!gameLoaded || !bot || disabledUiParts.includes('*')) return
|
||||
|
||||
return <>
|
||||
|
|
@ -116,6 +121,7 @@ const InGameUi = () => {
|
|||
{!disabledUiParts.includes('players-list') && <PlayerListOverlayProvider />}
|
||||
{!disabledUiParts.includes('chat') && <ChatProvider />}
|
||||
<SoundMuffler />
|
||||
{showMinimap !== 'never' && <MinimapProvider displayMode='minimapOnly' />}
|
||||
{!disabledUiParts.includes('title') && <TitleProvider />}
|
||||
{!disabledUiParts.includes('scoreboard') && <ScoreboardProvider />}
|
||||
{!disabledUiParts.includes('effects-indicators') && <IndicatorEffectsProvider />}
|
||||
|
|
@ -137,6 +143,7 @@ const InGameUi = () => {
|
|||
<DisplayQr />
|
||||
</PerComponentErrorBoundary>
|
||||
<RobustPortal to={document.body}>
|
||||
{displayFullmap && <MinimapProvider displayMode='fullmapOnly' />}
|
||||
{/* because of z-index */}
|
||||
{showUI && <TouchControls />}
|
||||
<GlobalSearchInput />
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ body {
|
|||
|
||||
@font-face {
|
||||
font-family: mojangles;
|
||||
src: url(../assets/mojangles.ttf);
|
||||
src: url(../assets/mojangles.ttf?inline);
|
||||
}
|
||||
|
||||
#ui-root {
|
||||
|
|
@ -181,6 +181,10 @@ body::xr-overlay #viewer-canvas {
|
|||
color: #999;
|
||||
}
|
||||
|
||||
.text-select {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 430px) {
|
||||
.span-2 {
|
||||
grid-column: span 2;
|
||||
|
|
|
|||
|
|
@ -88,13 +88,40 @@ export const statsEnd = () => {
|
|||
|
||||
// for advanced debugging, use with watch expression
|
||||
|
||||
window.statsPerSec = {}
|
||||
let statsPerSec = {}
|
||||
window.addStatPerSec = (name) => {
|
||||
statsPerSec[name] ??= 0
|
||||
statsPerSec[name]++
|
||||
window.statsPerSecAvg = {}
|
||||
let currentStatsPerSec = {} as Record<string, number[]>
|
||||
const waitingStatsPerSec = {}
|
||||
window.markStart = (label) => {
|
||||
waitingStatsPerSec[label] ??= []
|
||||
waitingStatsPerSec[label][0] = performance.now()
|
||||
}
|
||||
window.markEnd = (label) => {
|
||||
if (!waitingStatsPerSec[label]?.[0]) return
|
||||
currentStatsPerSec[label] ??= []
|
||||
currentStatsPerSec[label].push(performance.now() - waitingStatsPerSec[label][0])
|
||||
delete waitingStatsPerSec[label]
|
||||
}
|
||||
const updateStatsPerSecAvg = () => {
|
||||
window.statsPerSecAvg = Object.fromEntries(Object.entries(currentStatsPerSec).map(([key, value]) => {
|
||||
return [key, {
|
||||
avg: value.reduce((a, b) => a + b, 0) / value.length,
|
||||
count: value.length
|
||||
}]
|
||||
}))
|
||||
currentStatsPerSec = {}
|
||||
}
|
||||
|
||||
|
||||
window.statsPerSec = {}
|
||||
let statsPerSecCurrent = {}
|
||||
window.addStatPerSec = (name) => {
|
||||
statsPerSecCurrent[name] ??= 0
|
||||
statsPerSecCurrent[name]++
|
||||
}
|
||||
window.statsPerSecCurrent = statsPerSecCurrent
|
||||
setInterval(() => {
|
||||
window.statsPerSec = statsPerSec
|
||||
statsPerSec = {}
|
||||
window.statsPerSec = statsPerSecCurrent
|
||||
statsPerSecCurrent = {}
|
||||
window.statsPerSecCurrent = statsPerSecCurrent
|
||||
updateStatsPerSecAvg()
|
||||
}, 1000)
|
||||
|
|
|
|||
|
|
@ -96,6 +96,6 @@ export const watchOptionsAfterWorldViewInit = () => {
|
|||
watchValue(options, o => {
|
||||
if (!worldView) return
|
||||
worldView.keepChunksDistance = o.keepChunksDistance
|
||||
worldView.handDisplay = o.handDisplay
|
||||
viewer.world.config.displayHand = o.handDisplay
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@ customEvents.on('gameLoaded', () => {
|
|||
let sceneBg = { r: 0, g: 0, b: 0 }
|
||||
export const updateBackground = (newSceneBg = sceneBg) => {
|
||||
sceneBg = newSceneBg
|
||||
if (inWater) {
|
||||
viewer.scene.background = new THREE.Color(0x00_00_ff)
|
||||
} else {
|
||||
viewer.scene.background = new THREE.Color(sceneBg.r, sceneBg.g, sceneBg.b)
|
||||
}
|
||||
const color: [number, number, number] = inWater ? [0, 0, 1] : [sceneBg.r, sceneBg.g, sceneBg.b]
|
||||
viewer.world.changeBackgroundColor(color)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as THREE from 'three'
|
|||
|
||||
// wouldn't better to create atlas instead?
|
||||
import { Vec3 } from 'vec3'
|
||||
import { LineMaterial, Wireframe, LineSegmentsGeometry } from 'three-stdlib'
|
||||
import { LineMaterial } from 'three-stdlib'
|
||||
import { Entity } from 'prismarine-entity'
|
||||
import destroyStage0 from '../assets/destroy_stage_0.png'
|
||||
import destroyStage1 from '../assets/destroy_stage_1.png'
|
||||
|
|
@ -34,7 +34,6 @@ function getViewDirection (pitch, yaw) {
|
|||
|
||||
class WorldInteraction {
|
||||
ready = false
|
||||
interactionLines: null | { blockPos; mesh } = null
|
||||
prevBreakState
|
||||
currentDigTime
|
||||
prevOnGround
|
||||
|
|
@ -44,11 +43,9 @@ class WorldInteraction {
|
|||
lastButtons = [false, false, false]
|
||||
breakStartTime: number | undefined = 0
|
||||
lastDugBlock: Vec3 | null = null
|
||||
cursorBlock: import('prismarine-block').Block | null = null
|
||||
blockBreakMesh: THREE.Mesh
|
||||
breakTextures: THREE.Texture[]
|
||||
lastDigged: number
|
||||
lineMaterial: LineMaterial
|
||||
debugDigStatus: string
|
||||
|
||||
oneTimeInit () {
|
||||
|
|
@ -109,10 +106,10 @@ class WorldInteraction {
|
|||
})
|
||||
|
||||
beforeRenderFrame.push(() => {
|
||||
if (this.lineMaterial) {
|
||||
if (viewer.world.threejsCursorLineMaterial) {
|
||||
const { renderer } = viewer
|
||||
this.lineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height)
|
||||
this.lineMaterial.dashOffset = performance.now() / 750
|
||||
viewer.world.threejsCursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height)
|
||||
viewer.world.threejsCursorLineMaterial.dashOffset = performance.now() / 750
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -133,7 +130,7 @@ class WorldInteraction {
|
|||
this.debugDigStatus = 'done'
|
||||
})
|
||||
bot.on('diggingAborted', (block) => {
|
||||
if (!this.cursorBlock?.position.equals(block.position)) return
|
||||
if (!viewer.world.cursorBlock?.equals(block.position)) return
|
||||
this.debugDigStatus = 'aborted'
|
||||
// if (this.lastDugBlock)
|
||||
this.breakStartTime = undefined
|
||||
|
|
@ -151,7 +148,7 @@ class WorldInteraction {
|
|||
const upLineMaterial = () => {
|
||||
const inCreative = bot.game.gameMode === 'creative'
|
||||
const pixelRatio = viewer.renderer.getPixelRatio()
|
||||
this.lineMaterial = new LineMaterial({
|
||||
viewer.world.threejsCursorLineMaterial = new LineMaterial({
|
||||
color: inCreative ? 0x40_80_ff : 0x00_00_00,
|
||||
linewidth: Math.max(pixelRatio * 0.7, 1) * 2,
|
||||
// dashed: true,
|
||||
|
|
@ -192,34 +189,6 @@ class WorldInteraction {
|
|||
}
|
||||
}
|
||||
|
||||
updateBlockInteractionLines (blockPos: Vec3 | null, shapePositions?: Array<{ position; width; height; depth }>) {
|
||||
assertDefined(viewer)
|
||||
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
|
||||
return
|
||||
}
|
||||
if (this.interactionLines !== null) {
|
||||
viewer.scene.remove(this.interactionLines.mesh)
|
||||
this.interactionLines = null
|
||||
}
|
||||
if (blockPos === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
for (const { position, width, height, depth } of shapePositions ?? []) {
|
||||
const scale = [1.0001 * width, 1.0001 * height, 1.0001 * depth] as const
|
||||
const geometry = new THREE.BoxGeometry(...scale)
|
||||
const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry))
|
||||
const wireframe = new Wireframe(lines, this.lineMaterial)
|
||||
const pos = blockPos.plus(position)
|
||||
wireframe.position.set(pos.x, pos.y, pos.z)
|
||||
wireframe.computeLineDistances()
|
||||
group.add(wireframe)
|
||||
}
|
||||
viewer.scene.add(group)
|
||||
this.interactionLines = { blockPos, mesh: group }
|
||||
}
|
||||
|
||||
// todo this shouldnt be done in the render loop, migrate the code to dom events to avoid delays on lags
|
||||
update () {
|
||||
const inSpectator = bot.game.gameMode === 'spectator'
|
||||
|
|
@ -232,10 +201,7 @@ class WorldInteraction {
|
|||
let cursorBlockDiggable = cursorBlock
|
||||
if (cursorBlock && !bot.canDigBlock(cursorBlock) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null
|
||||
|
||||
let cursorChanged = !cursorBlock !== !this.cursorBlock
|
||||
if (cursorBlock && this.cursorBlock) {
|
||||
cursorChanged = !cursorBlock.position.equals(this.cursorBlock.position)
|
||||
}
|
||||
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) {
|
||||
|
|
@ -291,8 +257,8 @@ class WorldInteraction {
|
|||
bot.lookAt = oldLookAt
|
||||
}).catch(console.warn)
|
||||
}
|
||||
viewer.world.changeHandSwingingState(true)
|
||||
viewer.world.changeHandSwingingState(false)
|
||||
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
|
||||
|
|
@ -351,42 +317,36 @@ class WorldInteraction {
|
|||
})
|
||||
customEvents.emit('digStart')
|
||||
this.lastDigged = Date.now()
|
||||
viewer.world.changeHandSwingingState(true)
|
||||
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)
|
||||
viewer.world.changeHandSwingingState(false, false)
|
||||
}
|
||||
this.prevOnGround = onGround
|
||||
|
||||
// Show cursor
|
||||
const allShapes = [...cursorBlock?.shapes ?? [], ...cursorBlock?.['interactionShapes'] ?? []]
|
||||
if (cursorBlock) {
|
||||
const allShapes = [...cursorBlock.shapes, ...cursorBlock['interactionShapes'] ?? []]
|
||||
this.updateBlockInteractionLines(cursorBlock.position, allShapes.map(shape => {
|
||||
return getDataFromShape(shape)
|
||||
}))
|
||||
{
|
||||
// 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)
|
||||
}
|
||||
} else {
|
||||
this.updateBlockInteractionLines(null)
|
||||
// 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
|
||||
|
|
@ -411,7 +371,11 @@ class WorldInteraction {
|
|||
}
|
||||
|
||||
// Update state
|
||||
this.cursorBlock = cursorBlock
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -29,6 +29,6 @@
|
|||
"prismarine-viewer/examples"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue