This commit is contained in:
Vitaly 2024-12-14 10:34:49 +03:00 committed by GitHub
commit 8dd6715b3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 7314 additions and 478 deletions

View file

@ -6,3 +6,4 @@ generated
dist
public
**/*/rsbuildSharedConfig.ts
src/mcDataTypes.ts

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -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

View file

@ -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>>

View file

@ -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

View file

@ -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
}

View 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)
}

View file

@ -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
}
}
}

View file

@ -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)

View file

@ -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: {} })

View file

@ -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 }

View file

@ -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())

View file

@ -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,

View file

@ -0,0 +1 @@
export const INVISIBLE_BLOCKS = new Set(['air', 'void_air', 'cave_air', 'barrier'])

View file

@ -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 () {

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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')
}

View file

@ -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')

View file

@ -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',

View file

@ -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'
}
]

View file

@ -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'
}

View file

@ -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)

View file

@ -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
}

View file

@ -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) {

View file

@ -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

View file

@ -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}`)

File diff suppressed because one or more lines are too long

View file

@ -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>

View file

@ -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

View file

@ -38,10 +38,10 @@ export default ({
return (
<Screen
className='small-content'
titleSelectable={isError}
title={
<>
<span style={{
userSelect: isError ? 'text' : undefined,
wordBreak: 'break-word',
}}
>

View file

@ -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>

View file

@ -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
View 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
View 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>
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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()

View 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
View 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
View 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)
}
}

View 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} />
}

View file

@ -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>

View file

@ -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
}

View file

@ -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>

View file

@ -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;

View file

@ -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])
}

View file

@ -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

View file

@ -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)
}

View file

@ -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 />

View file

@ -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;

View file

@ -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)

View file

@ -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
})
}

View file

@ -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)
}

View file

@ -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]

View file

@ -29,6 +29,6 @@
"prismarine-viewer/examples"
],
"exclude": [
"node_modules",
"node_modules"
]
}