Compare commits

..

37 commits

Author SHA1 Message Date
Vitaly Turovsky
253e094c74 add zardoy/mwc-proxy repo ref 2025-10-11 02:25:14 +03:00
Vitaly Turovsky
fef94f03fb feat: add support for alt+arrows navigation to navigate between commands only 2025-10-11 02:25:06 +03:00
Vitaly Turovsky
e9f91f8ecd feat: enable music by default, add slider for controlling its volume 2025-10-11 02:24:51 +03:00
Colbster937
634df8d03d
Add WebMC & WS changes (#431)
Co-authored-by: Colbster937 <96893162+colbychittenden@users.noreply.github.com>
2025-10-11 01:52:06 +03:00
Vitaly Turovsky
a88c8b5470 possible fix for rare edgecase where skins from server were not applied. Cause: renderer due to rare circumnstances could be loaded AFTER gameLoaded which is fired only when starting rendering 3d world. classic no existing data handling issue
why not mineflayerBotCreated? because getThreeJsRendererMethods not available at that time so would make things only much complex
2025-09-30 09:38:37 +03:00
Vitaly Turovsky
f51254d97a fix: dont stop local replay server with keep alive connection error 2025-09-30 07:20:30 +03:00
Vitaly Turovsky
05cd560d6b add shadow and directional light for player in inventory (model viewer) 2025-09-29 02:01:04 +03:00
Vitaly Turovsky
b239636356 feat: add debugServerPacketNames and debugClientPacketNames for quick access of names with intellisense of packets for current protocol. Should be used with window.inspectPacket in console 2025-09-28 22:04:17 +03:00
Vitaly Turovsky
4f421ae45f respect loadPlayerSkins option for inventory skin 2025-09-28 21:59:00 +03:00
Vitaly
3b94889bed
feat: make arrows colorful and metadata (#430)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-09-20 02:57:59 +03:00
Vitaly
636a7fdb54
feat: improve fog a little (#427) 2025-09-19 05:42:22 +03:00
Vitaly Turovsky
c930365e32 fix sometimes inventory player should not be rendered 2025-09-18 07:49:44 +03:00
Vitaly Turovsky
852dd737ae fix: fix some UI like error screen was not visible fully (buttons were clipped behind the screen) on larger scale on large screens 2025-09-11 22:24:04 +03:00
Vitaly Turovsky
06dc3cb033 feat: Add saveLoginPassword option to control password saving behavior in browser for offline auth on servers 2025-09-08 05:38:16 +03:00
Vitaly Turovsky
c4097975bf add a way to disable sky box for old behavior (not tested) 2025-09-08 05:29:34 +03:00
Vitaly Turovsky
1525fac2a1 fix: some visual camera world view issues (visible lines between blocks) 2025-09-08 05:22:24 +03:00
Vitaly Turovsky
f24cb49a87 up lockfile 2025-09-08 04:55:43 +03:00
Vitaly Turovsky
0b1183f541 up minecraft-data 2025-09-08 04:36:09 +03:00
Vitaly Turovsky
739a6fad24 fix lockfile 2025-09-08 04:34:17 +03:00
Vitaly Turovsky
7f7a14ac65 feat: Add overlay model viewer. Already integrated into inventory to display player! 2025-09-08 04:19:38 +03:00
Vitaly
265d02d18d up protocol for 1.21.8 2025-09-07 18:23:13 +00:00
Vitaly
b2e36840b9
feat: brand new default skybox with fog, better daycycle and colors (#425)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-05 05:02:54 +03:00
Vitaly
7043bf49f3
fix: adding support for newer skin profile data structure in player heads (#424) 2025-09-04 21:55:34 +03:00
Vitaly
528d8f516b
Update worldrendererThree.ts 2025-09-04 21:55:02 +03:00
Kesuaheli
70534d8b5a
fix: adding support for newer skin profile data structure in player heads 2025-09-04 12:51:56 +02:00
Vitaly Turovsky
9d54c70fb7 use node 22 2025-09-02 19:05:18 +03:00
Vitaly Turovsky
7e3ba8bece up integrated server for the latest fixes and better stability 2025-09-02 19:02:30 +03:00
Vitaly
513201be87 up browserify 2025-09-01 08:56:08 +03:00
Vitaly Turovsky
cb82188272 add addPing query param for testing 2025-08-31 19:31:26 +03:00
Vitaly Turovsky
d0d5234ba4 fix: stop right click emulation once window got opened eg chest 2025-08-31 18:31:49 +03:00
Vitaly Turovsky
e81d608554 fix cname 2025-08-27 19:52:09 +03:00
Vitaly Turovsky
1f240d8c20 up mouse allowing to disable positive break block 2025-08-27 19:50:53 +03:00
Vitaly Turovsky
2a1746eb7a [skip ci] fix repository name 2025-08-27 13:33:39 +03:00
Vitaly Turovsky
9718610131 ci: add deployment step for mcw-mcraft-page repository in GitHub Actions 2025-08-27 12:08:20 +03:00
Vitaly Turovsky
8f62fbd4da fix: window title sometimes was not showing up on old versions 2025-08-26 13:50:36 +03:00
Vitaly Turovsky
bc2972fe99 fix registering custom channels too late (a few ms diff) 2025-08-24 19:53:44 +03:00
Vitaly
a12c61bc6c
add simple monaco (#418) 2025-08-21 13:21:02 +03:00
44 changed files with 1476 additions and 241 deletions

View file

@ -26,7 +26,7 @@ jobs:
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: "pnpm"
- name: Move Cypress to dependencies
run: |

View file

@ -49,6 +49,20 @@ jobs:
publish_dir: .vercel/output/static
force_orphan: true
# Create CNAME file for custom domain
- name: Create CNAME file
run: echo "github.mcraft.fun" > .vercel/output/static/CNAME
- name: Deploy to mwc-mcraft-pages repository
uses: peaceiris/actions-gh-pages@v3
with:
personal_token: ${{ secrets.MCW_MCRAFT_PAGE_DEPLOY_TOKEN }}
external_repository: ${{ github.repository_owner }}/mwc-mcraft-pages
publish_dir: .vercel/output/static
publish_branch: main
destination_dir: docs
force_orphan: true
- name: Change index.html title
run: |
# change <title>Minecraft Web Client</title> to <title>Minecraft Web Client — Free Online Browser Version</title>

View file

@ -78,6 +78,8 @@ There is a builtin proxy, but you can also host your one! Just clone the repo, r
[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
> **Note**: If you want to make **your own** Minecraft server accessible to web clients (without our proxies), you can use [mwc-proxy](https://github.com/zardoy/mwc-proxy) - a lightweight JS WebSocket proxy that runs on the same server as your Minecraft server, allowing players to connect directly via `wss://play.example.com`. `?client_mcraft` is added to the URL, so the proxy will know that it's this client.
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
```mermaid
@ -176,6 +178,7 @@ Server specific:
- `?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.
- `?autoConnect=true` - Only works then `ip` and `version` parameters are set and `allowAutoConnect` is `true` in config.json! Directly connects to the specified server. Useful for integrates iframes.
- `?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.
- `?addPing=<ping>` - Add a latency to both sides of the connection. Useful for testing ping issues. For example `?addPing=100` will add 200ms to your ping.
Single player specific:
@ -232,3 +235,4 @@ Only during development:
- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true)
- [https://m.eaglercraft.com/](EaglerCraft) - Eaglercraft runnable on mobile (real Minecraft in the browser)
- [js-minecraft](https://github.com/LabyStudio/js-minecraft) - An insanely well done clone from the graphical side that inspired many features here

View file

@ -10,6 +10,10 @@
{
"ip": "wss://play.mcraft.fun"
},
{
"ip": "wss://play.webmc.fun",
"name": "WebMC"
},
{
"ip": "wss://ws.fuchsmc.net"
},

View file

@ -80,14 +80,14 @@
"esbuild-plugin-polyfill-node": "^0.3.0",
"express": "^4.18.2",
"filesize": "^10.0.12",
"flying-squid": "npm:@zardoy/flying-squid@^0.0.62",
"flying-squid": "npm:@zardoy/flying-squid@^0.0.104",
"framer-motion": "^12.9.2",
"fs-extra": "^11.1.1",
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"mcraft-fun-mineflayer": "^0.1.23",
"minecraft-data": "3.92.0",
"minecraft-data": "3.98.0",
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
"mojangson": "^2.0.4",
@ -157,7 +157,7 @@
"mc-assets": "^0.2.62",
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
"mineflayer-mouse": "^0.1.17",
"mineflayer-mouse": "^0.1.21",
"npm-run-all": "^4.1.5",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
@ -205,7 +205,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.92.0",
"minecraft-data": "3.98.0",
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
"prismarine-physics": "github:zardoy/prismarine-physics",
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",

148
pnpm-lock.yaml generated
View file

@ -13,7 +13,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.92.0
minecraft-data: 3.98.0
prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything
prismarine-physics: github:zardoy/prismarine-physics
minecraft-protocol: github:PrismarineJS/node-minecraft-protocol#master
@ -121,8 +121,8 @@ importers:
specifier: ^10.0.12
version: 10.1.6
flying-squid:
specifier: npm:@zardoy/flying-squid@^0.0.62
version: '@zardoy/flying-squid@0.0.62(encoding@0.1.13)'
specifier: npm:@zardoy/flying-squid@^0.0.104
version: '@zardoy/flying-squid@0.0.104(encoding@0.1.13)'
framer-motion:
specifier: ^12.9.2
version: 12.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -140,13 +140,13 @@ importers:
version: 4.17.21
mcraft-fun-mineflayer:
specifier: ^0.1.23
version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13))
version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13))
minecraft-data:
specifier: 3.92.0
version: 3.92.0
specifier: 3.98.0
version: 3.98.0
minecraft-protocol:
specifier: github:PrismarineJS/node-minecraft-protocol#master
version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
mineflayer-item-map-downloader:
specifier: github:zardoy/mineflayer-item-map-downloader
version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13)
@ -155,7 +155,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/17fb901e8ea480a52c8fd46373695be172be8aa5
version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618
node-gzip:
specifier: ^1.1.2
version: 1.1.2
@ -170,7 +170,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/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0)
prosemirror-example-setup:
specifier: ^1.2.2
version: 1.2.3
@ -345,10 +345,10 @@ importers:
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1)
mineflayer:
specifier: github:zardoy/mineflayer#gen-the-master
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
mineflayer-mouse:
specifier: ^0.1.17
version: 0.1.17
specifier: ^0.1.21
version: 0.1.21
npm-run-all:
specifier: ^4.1.5
version: 4.1.5
@ -436,7 +436,7 @@ importers:
version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-chunk:
specifier: github:zardoy/prismarine-chunk#master
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
prismarine-schematic:
specifier: ^1.2.0
version: 1.2.3
@ -3387,13 +3387,13 @@ 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.49':
resolution: {integrity: sha512-Kt4wr5/R+44tcLU9gjuNG2an9weWeKEpIoKXfsgJN2GGQqdnbd5nBpxfGDdgZ9aMdFugsVW8BsyPZNhj9vbMXA==}
'@zardoy/flying-squid@0.0.104':
resolution: {integrity: sha512-jGhQ7fn7o8UN+mUwZbt9674D37YLuBi+Au4TwKcopCA6huIQdHTFNl2e+0ZSTI5mnhN+NpyVoR3vmtH6L58vHQ==}
engines: {node: '>=8'}
hasBin: true
'@zardoy/flying-squid@0.0.62':
resolution: {integrity: sha512-M6icydO/yrmwevBhmgKcqEPC63AhWfU/Es9N/uadVrmKaxGm2FQMMLcybbutRYm1xZ6qsdxDUOUZnN56PsVwfQ==}
'@zardoy/flying-squid@0.0.49':
resolution: {integrity: sha512-Kt4wr5/R+44tcLU9gjuNG2an9weWeKEpIoKXfsgJN2GGQqdnbd5nBpxfGDdgZ9aMdFugsVW8BsyPZNhj9vbMXA==}
engines: {node: '>=8'}
hasBin: true
@ -6444,6 +6444,12 @@ packages:
resolution: {integrity: sha512-RYZeD1+joNlPuUpi+tIWkbP0ieVJr+R6IFkI6/8juhSxx9zE4osoSmteybrfspGm8A6u+YbbY1epqRKEMwVR6Q==}
engines: {node: '>=18.0.0'}
mc-bridge@0.1.3:
resolution: {integrity: sha512-H9jPt2xEU77itC27dSz3qazHYqN9qVsv4HgMPozg7RqQ1uwgXmEa+ojKIlDtXf/TLJsG6Kv4EbzGa8a1Wh72uA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
minecraft-data: 3.98.0
mcraft-fun-mineflayer@0.1.23:
resolution: {integrity: sha512-qmI1rQQ0Ro5zJdi99rClWLF+mS9JZffgNX2vyWWesS3Hsk3Xblp/8swYTJKHSaFpNgzkVfXV92fEIrBqeH6iKA==}
version: 0.1.23
@ -6652,8 +6658,8 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
minecraft-data@3.92.0:
resolution: {integrity: sha512-CGfO50svzm+pSRa4Mbq4owsmRKbPCNkSZ3MCOyH+epC7yNjh+PUhPQFHWq72O51qsY7pAB5qM/bJn1ncwG1J5g==}
minecraft-data@3.98.0:
resolution: {integrity: sha512-JAPqJ/TZoxMUlAPPdWUh1v5wdqvYGFSZ4rW9bUtmaKBkGpomDSjw4V02ocBqbxKJvcTtmc5nM/LfN9/0DDqHrQ==}
minecraft-folder-path@1.2.0:
resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==}
@ -6662,9 +6668,9 @@ packages:
resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41}
version: 1.0.1
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074:
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074}
version: 1.61.0
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9:
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9}
version: 1.62.0
engines: {node: '>=22'}
minecraft-wrap@1.6.0:
@ -6678,12 +6684,12 @@ packages:
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824}
version: 1.2.0
mineflayer-mouse@0.1.17:
resolution: {integrity: sha512-0eCR8pnGb42Qd9QmAxOjl0PhA5Fa+9+6H1G/YsbsO5rg5mDf94Tusqp/8NAGLPQCPVDzbarLskXdjR3h0E0bEQ==}
mineflayer-mouse@0.1.21:
resolution: {integrity: sha512-1XTVuw3twIrEcqQ1QRSB8NcStIUEZ+tbxiAG6rOrN/9M4thhtlS5PTJzFdmdrcYgWEBLvuOdJszaKE5zFfiXhg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8:
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8}
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659:
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659}
version: 8.0.0
engines: {node: '>=22'}
@ -6849,8 +6855,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/17fb901e8ea480a52c8fd46373695be172be8aa5:
resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5}
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618:
resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618}
version: 0.2.4
nice-try@1.0.5:
@ -7381,7 +7387,7 @@ packages:
prismarine-biome@1.3.0:
resolution: {integrity: sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==}
peerDependencies:
minecraft-data: 3.92.0
minecraft-data: 3.98.0
prismarine-registry: ^1.1.0
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9:
@ -11337,8 +11343,8 @@ snapshots:
'@nxg-org/mineflayer-trajectories@1.2.0(encoding@0.1.13)':
dependencies:
'@nxg-org/mineflayer-util-plugin': 1.8.4
minecraft-data: 3.92.0
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)
minecraft-data: 3.98.0
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-item: 1.17.0
prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b
@ -13088,7 +13094,7 @@ snapshots:
'@types/emscripten': 1.40.0
tslib: 1.14.1
'@zardoy/flying-squid@0.0.49(encoding@0.1.13)':
'@zardoy/flying-squid@0.0.104(encoding@0.1.13)':
dependencies:
'@tootallnate/once': 2.0.0
chalk: 5.4.1
@ -13098,16 +13104,18 @@ snapshots:
exit-hook: 2.2.1
flatmap: 0.0.3
long: 5.3.1
minecraft-data: 3.92.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
mc-bridge: 0.1.3(minecraft-data@3.98.0)
minecraft-data: 3.98.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(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/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
prismarine-entity: 2.5.0
prismarine-item: 1.17.0
prismarine-nbt: 2.7.0
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0)
prismarine-windows: 2.9.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
rambda: 9.4.2
@ -13124,7 +13132,7 @@ snapshots:
- encoding
- supports-color
'@zardoy/flying-squid@0.0.62(encoding@0.1.13)':
'@zardoy/flying-squid@0.0.49(encoding@0.1.13)':
dependencies:
'@tootallnate/once': 2.0.0
chalk: 5.4.1
@ -13134,16 +13142,16 @@ snapshots:
exit-hook: 2.2.1
flatmap: 0.0.3
long: 5.3.1
minecraft-data: 3.92.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
minecraft-data: 3.98.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(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/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
prismarine-entity: 2.5.0
prismarine-item: 1.17.0
prismarine-nbt: 2.7.0
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0)
prismarine-windows: 2.9.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
rambda: 9.4.2
@ -14534,8 +14542,8 @@ snapshots:
diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71:
dependencies:
minecraft-data: 3.92.0
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
minecraft-data: 3.98.0
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
prismarine-registry: 1.11.0
random-seed: 0.3.0
vec3: 0.1.10
@ -16978,12 +16986,16 @@ snapshots:
maxrects-packer: '@zardoy/maxrects-packer@2.7.4'
zod: 3.24.2
mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)):
mc-bridge@0.1.3(minecraft-data@3.98.0):
dependencies:
minecraft-data: 3.98.0
mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)):
dependencies:
'@zardoy/flying-squid': 0.0.49(encoding@0.1.13)
exit-hook: 2.2.1
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
prismarine-item: 1.17.0
ws: 8.18.1
transitivePeerDependencies:
@ -17290,7 +17302,7 @@ snapshots:
min-indent@1.0.1: {}
minecraft-data@3.92.0: {}
minecraft-data@3.98.0: {}
minecraft-folder-path@1.2.0: {}
@ -17301,7 +17313,7 @@ snapshots:
- '@types/react'
- react
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13):
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13):
dependencies:
'@types/node-rsa': 1.1.4
'@types/readable-stream': 4.0.18
@ -17310,7 +17322,7 @@ snapshots:
debug: 4.4.0(supports-color@8.1.1)
endian-toggle: 0.0.0
lodash.merge: 4.6.2
minecraft-data: 3.92.0
minecraft-data: 3.98.0
minecraft-folder-path: 1.2.0
node-fetch: 2.7.0(encoding@0.1.13)
node-rsa: 0.4.2
@ -17353,13 +17365,13 @@ snapshots:
mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13):
dependencies:
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
sharp: 0.30.7
transitivePeerDependencies:
- encoding
- supports-color
mineflayer-mouse@0.1.17:
mineflayer-mouse@0.1.21:
dependencies:
change-case: 5.4.4
debug: 4.4.1
@ -17368,15 +17380,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13):
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13):
dependencies:
'@nxg-org/mineflayer-physics-util': 1.8.10
minecraft-data: 3.92.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
minecraft-data: 3.98.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-chat: 1.11.0
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
prismarine-entity: 2.5.0
prismarine-item: 1.17.0
prismarine-nbt: 2.7.0
@ -17574,7 +17586,7 @@ snapshots:
neo-async@2.6.2: {}
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5:
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618:
dependencies:
body-parser: 1.20.3
express: 4.21.2
@ -18162,15 +18174,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
prismarine-biome@1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0):
prismarine-biome@1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0):
dependencies:
minecraft-data: 3.92.0
minecraft-data: 3.98.0
prismarine-registry: 1.11.0
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9:
dependencies:
minecraft-data: 3.92.0
prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
minecraft-data: 3.98.0
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0)
prismarine-chat: 1.11.0
prismarine-item: 1.17.0
prismarine-nbt: 2.7.0
@ -18182,9 +18194,9 @@ snapshots:
prismarine-nbt: 2.7.0
prismarine-registry: 1.11.0
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0):
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0):
dependencies:
prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-nbt: 2.7.0
prismarine-registry: 1.11.0
@ -18213,14 +18225,14 @@ snapshots:
prismarine-physics@https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b:
dependencies:
minecraft-data: 3.92.0
minecraft-data: 3.98.0
prismarine-nbt: 2.7.0
vec3: 0.1.10
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0):
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0):
dependencies:
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
prismarine-nbt: 2.7.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
uint4: 0.1.2
@ -18242,13 +18254,13 @@ snapshots:
prismarine-registry@1.11.0:
dependencies:
minecraft-data: 3.92.0
minecraft-data: 3.98.0
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-nbt: 2.7.0
prismarine-schematic@1.2.3:
dependencies:
minecraft-data: 3.92.0
minecraft-data: 3.98.0
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-nbt: 2.7.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c

View file

@ -0,0 +1,55 @@
import { PlayerObject, PlayerAnimation } from 'skinview3d'
import * as THREE from 'three'
import { WalkingGeneralSwing } from '../three/entity/animations'
import { loadSkinImage, stevePngUrl } from './utils/skins'
export type PlayerObjectType = PlayerObject & {
animation?: PlayerAnimation
realPlayerUuid: string
realUsername: string
}
export function createPlayerObject (options: {
username?: string
uuid?: string
scale?: number
}): {
playerObject: PlayerObjectType
wrapper: THREE.Group
} {
const wrapper = new THREE.Group()
const playerObject = new PlayerObject() as PlayerObjectType
playerObject.realPlayerUuid = options.uuid ?? ''
playerObject.realUsername = options.username ?? ''
playerObject.position.set(0, 16, 0)
// fix issues with starfield
playerObject.traverse((obj) => {
if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) {
obj.material.transparent = true
}
})
wrapper.add(playerObject as any)
const scale = options.scale ?? (1 / 16)
wrapper.scale.set(scale, scale, scale)
wrapper.rotation.set(0, Math.PI, 0)
// Set up animation
playerObject.animation = new WalkingGeneralSwing()
;(playerObject.animation as WalkingGeneralSwing).isMoving = false
playerObject.animation.update(playerObject, 0)
return { playerObject, wrapper }
}
export const applySkinToPlayerObject = async (playerObject: PlayerObjectType, skinUrl: string) => {
return loadSkinImage(skinUrl || stevePngUrl).then(({ canvas }) => {
const skinTexture = new THREE.CanvasTexture(canvas)
skinTexture.magFilter = THREE.NearestFilter
skinTexture.minFilter = THREE.NearestFilter
skinTexture.needsUpdate = true
playerObject.skin.map = skinTexture as any
}).catch(console.error)
}

View file

@ -7,6 +7,7 @@ import { Vec3 } from 'vec3'
import { BotEvents } from 'mineflayer'
import { proxy } from 'valtio'
import TypedEmitter from 'typed-emitter'
import { Biome } from 'minecraft-data'
import { delayedIterator } from '../../playground/shared'
import { chunkPos } from './simpleUtils'
@ -28,6 +29,8 @@ export type WorldDataEmitterEvents = {
updateLight: (data: { pos: Vec3 }) => void
onWorldSwitch: () => void
end: () => void
biomeUpdate: (data: { biome: Biome }) => void
biomeReset: () => void
}
export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
@ -360,8 +363,37 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
delete this.debugChunksInfo[`${pos.x},${pos.z}`]
}
lastBiomeId: number | null = null
udpateBiome (pos: Vec3) {
try {
const biomeId = this.world.getBiome(pos)
if (biomeId !== this.lastBiomeId) {
this.lastBiomeId = biomeId
const biomeData = loadedData.biomes[biomeId]
if (biomeData) {
this.emitter.emit('biomeUpdate', {
biome: biomeData
})
} else {
// unknown biome
this.emitter.emit('biomeReset')
}
}
} catch (e) {
console.error('error updating biome', e)
}
}
lastPosCheck: Vec3 | null = null
async updatePosition (pos: Vec3, force = false) {
if (!this.allowPositionUpdate) return
const posFloored = pos.floored()
if (!force && this.lastPosCheck && this.lastPosCheck.equals(posFloored)) return
this.lastPosCheck = posFloored
this.udpateBiome(pos)
const [lastX, lastZ] = chunkPos(this.lastPos)
const [botX, botZ] = chunkPos(pos)
if (lastX !== botX || lastZ !== botZ || force) {

View file

@ -32,31 +32,45 @@ const toMajorVersion = version => {
export const worldCleanup = buildCleanupDecorator('resetWorld')
export const defaultWorldRendererConfig = {
// Debug settings
showChunkBorders: false,
enableDebugOverlay: false,
// Performance settings
mesherWorkers: 4,
isPlayground: false,
renderEars: true,
skinTexturesProxy: undefined as string | undefined,
// game renderer setting actually
showHand: false,
viewBobbing: false,
extraBlockRenderers: true,
clipWorldBelowY: undefined as number | undefined,
addChunksBatchWaitTime: 200,
_experimentalSmoothChunkLoading: true,
_renderByChunks: false,
// Rendering engine settings
dayCycle: true,
smoothLighting: true,
enableLighting: true,
starfield: true,
addChunksBatchWaitTime: 200,
defaultSkybox: true,
renderEntities: true,
extraBlockRenderers: true,
foreground: true,
fov: 75,
volume: 1,
// Camera visual related settings
showHand: false,
viewBobbing: false,
renderEars: true,
highlightBlockColor: 'blue',
// Player models
fetchPlayerSkins: true,
skinTexturesProxy: undefined as string | undefined,
// VR settings
vrSupport: true,
vrPageGameRendering: true,
renderEntities: true,
fov: 75,
fetchPlayerSkins: true,
highlightBlockColor: 'blue',
foreground: true,
enableDebugOverlay: false,
_experimentalSmoothChunkLoading: true,
_renderByChunks: false,
volume: 1
// World settings
clipWorldBelowY: undefined as number | undefined,
isPlayground: false
}
export type WorldRendererConfig = typeof defaultWorldRendererConfig
@ -496,6 +510,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
timeUpdated? (newTime: number): void
biomeUpdated? (biome: any): void
biomeReset? (): void
updateViewerPosition (pos: Vec3) {
this.viewerChunkPosition = pos
for (const [key, value] of Object.entries(this.loadedChunks)) {
@ -817,12 +835,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
})
worldEmitter.on('time', (timeOfDay) => {
if (!this.worldRendererConfig.dayCycle) return
this.timeUpdated?.(timeOfDay)
if (timeOfDay < 0 || timeOfDay > 24_000) {
throw new Error('Invalid time of day. It should be between 0 and 24000.')
}
this.timeOfTheDay = timeOfDay
// if (this.worldRendererConfig.skyLight === skyLight) return
@ -831,6 +846,14 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
// (this).rerenderAllChunks?.()
// }
})
worldEmitter.on('biomeUpdate', ({ biome }) => {
this.biomeUpdated?.(biome)
})
worldEmitter.on('biomeReset', () => {
this.biomeReset?.()
})
}
setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {

View file

@ -80,8 +80,12 @@ export class CameraShake {
camera.setRotationFromQuaternion(yawQuat)
} else {
// For regular camera, apply all rotations
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.basePitch)
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw)
// Add tiny offsets to prevent z-fighting at ideal angles (90, 180, 270 degrees)
const pitchOffset = this.addAntiZfightingOffset(this.basePitch)
const yawOffset = this.addAntiZfightingOffset(this.baseYaw)
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset)
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset)
const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle))
// Combine rotations in the correct order: pitch -> yaw -> roll
const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat)
@ -96,4 +100,21 @@ export class CameraShake {
private easeInOut (t: number): number {
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
}
private addAntiZfightingOffset (angle: number): number {
const offset = 0.001 // Very small offset in radians (about 0.057 degrees)
// Check if the angle is close to ideal angles (0, π/2, π, 3π/2)
const normalizedAngle = ((angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2)
const tolerance = 0.01 // Tolerance for considering an angle "ideal"
if (Math.abs(normalizedAngle) < tolerance ||
Math.abs(normalizedAngle - Math.PI / 2) < tolerance ||
Math.abs(normalizedAngle - Math.PI) < tolerance ||
Math.abs(normalizedAngle - 3 * Math.PI / 2) < tolerance) {
return angle + offset
}
return angle
}
}

View file

@ -20,6 +20,7 @@ import { ItemSpecificContextProperties } from '../lib/basePlayerState'
import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins'
import { renderComponent } from '../sign-renderer'
import { createCanvas } from '../lib/utils'
import { PlayerObjectType } from '../lib/createPlayerObject'
import { getBlockMeshFromModel } from './holdingBlock'
import { createItemMesh } from './itemMesh'
import * as Entity from './entity/EntityMesh'
@ -33,12 +34,6 @@ export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
export const TWEEN_DURATION = 120
type PlayerObjectType = PlayerObject & {
animation?: PlayerAnimation
realPlayerUuid: string
realUsername: string
}
function convert2sComplementToHex (complement: number) {
if (complement < 0) {
complement = (0xFF_FF_FF_FF + complement + 1) >>> 0

View file

@ -1,10 +1,51 @@
import * as THREE from 'three'
import { DebugGui } from '../lib/DebugGui'
export const DEFAULT_TEMPERATURE = 0.75
export class SkyboxRenderer {
private texture: THREE.Texture | null = null
private mesh: THREE.Mesh<THREE.SphereGeometry, THREE.MeshBasicMaterial> | null = null
private skyMesh: THREE.Mesh | null = null
private voidMesh: THREE.Mesh | null = null
constructor (private readonly scene: THREE.Scene, public initialImage: string | null) {}
// World state
private worldTime = 0
private partialTicks = 0
private viewDistance = 4
private temperature = DEFAULT_TEMPERATURE
private inWater = false
private waterBreathing = false
private fogBrightness = 0
private prevFogBrightness = 0
private readonly fogOrangeness = 0 // Debug property to control sky color orangeness
private readonly distanceFactor = 2.7
private readonly brightnessAtPosition = 1
debugGui: DebugGui
constructor (private readonly scene: THREE.Scene, public defaultSkybox: boolean, public initialImage: string | null) {
this.debugGui = new DebugGui('skybox_renderer', this, [
'temperature',
'worldTime',
'inWater',
'waterBreathing',
'fogOrangeness',
'brightnessAtPosition',
'distanceFactor'
], {
brightnessAtPosition: { min: 0, max: 1, step: 0.01 },
temperature: { min: 0, max: 1, step: 0.01 },
worldTime: { min: 0, max: 24_000, step: 1 },
fogOrangeness: { min: -1, max: 1, step: 0.01 },
distanceFactor: { min: 0, max: 5, step: 0.01 },
})
if (!initialImage) {
this.createGradientSky()
}
// this.debugGui.activate()
}
async init () {
if (this.initialImage) {
@ -58,10 +99,288 @@ export class SkyboxRenderer {
}
}
update (cameraPosition: THREE.Vector3) {
if (this.mesh) {
this.mesh.position.copy(cameraPosition)
update (cameraPosition: THREE.Vector3, newViewDistance: number) {
if (newViewDistance !== this.viewDistance) {
this.viewDistance = newViewDistance
this.updateSkyColors()
}
if (this.mesh) {
// Update skybox position
this.mesh.position.copy(cameraPosition)
} else if (this.skyMesh) {
// Update gradient sky position
this.skyMesh.position.copy(cameraPosition)
this.voidMesh?.position.copy(cameraPosition)
this.updateSkyColors() // Update colors based on time of day
}
}
// Update world time
updateTime (timeOfDay: number, partialTicks = 0) {
if (this.debugGui.visible) return
this.worldTime = timeOfDay
this.partialTicks = partialTicks
this.updateSkyColors()
}
// Update view distance
updateViewDistance (viewDistance: number) {
this.viewDistance = viewDistance
this.updateSkyColors()
}
// Update temperature (for biome support)
updateTemperature (temperature: number) {
if (this.debugGui.visible) return
this.temperature = temperature
this.updateSkyColors()
}
// Update water state
updateWaterState (inWater: boolean, waterBreathing: boolean) {
if (this.debugGui.visible) return
this.inWater = inWater
this.waterBreathing = waterBreathing
this.updateSkyColors()
}
// Update default skybox setting
updateDefaultSkybox (defaultSkybox: boolean) {
if (this.debugGui.visible) return
this.defaultSkybox = defaultSkybox
this.updateSkyColors()
}
private createGradientSky () {
const size = 64
const scale = 256 / size + 2
{
const geometry = new THREE.PlaneGeometry(size * scale * 2, size * scale * 2)
geometry.rotateX(-Math.PI / 2)
geometry.translate(0, 16, 0)
const material = new THREE.MeshBasicMaterial({
color: 0xff_ff_ff,
side: THREE.DoubleSide,
depthTest: false
})
this.skyMesh = new THREE.Mesh(geometry, material)
this.scene.add(this.skyMesh)
}
{
const geometry = new THREE.PlaneGeometry(size * scale * 2, size * scale * 2)
geometry.rotateX(-Math.PI / 2)
geometry.translate(0, -16, 0)
const material = new THREE.MeshBasicMaterial({
color: 0xff_ff_ff,
side: THREE.DoubleSide,
depthTest: false
})
this.voidMesh = new THREE.Mesh(geometry, material)
this.scene.add(this.voidMesh)
}
this.updateSkyColors()
}
private getFogColor (partialTicks = 0): THREE.Vector3 {
const angle = this.getCelestialAngle(partialTicks)
let rotation = Math.cos(angle * Math.PI * 2) * 2 + 0.5
rotation = Math.max(0, Math.min(1, rotation))
let x = 0.752_941_2
let y = 0.847_058_83
let z = 1
x *= (rotation * 0.94 + 0.06)
y *= (rotation * 0.94 + 0.06)
z *= (rotation * 0.91 + 0.09)
return new THREE.Vector3(x, y, z)
}
private getSkyColor (x = 0, z = 0, partialTicks = 0): THREE.Vector3 {
const angle = this.getCelestialAngle(partialTicks)
let brightness = Math.cos(angle * 3.141_593 * 2) * 2 + 0.5
if (brightness < 0) brightness = 0
if (brightness > 1) brightness = 1
const temperature = this.getTemperature(x, z)
const rgb = this.getSkyColorByTemp(temperature)
const red = ((rgb >> 16) & 0xff) / 255
const green = ((rgb >> 8) & 0xff) / 255
const blue = (rgb & 0xff) / 255
return new THREE.Vector3(
red * brightness,
green * brightness,
blue * brightness
)
}
private calculateCelestialAngle (time: number, partialTicks: number): number {
const modTime = (time % 24_000)
let angle = (modTime + partialTicks) / 24_000 - 0.25
if (angle < 0) {
angle++
}
if (angle > 1) {
angle--
}
angle = 1 - ((Math.cos(angle * Math.PI) + 1) / 2)
angle += (angle - angle) / 3
return angle
}
private getCelestialAngle (partialTicks: number): number {
return this.calculateCelestialAngle(this.worldTime, partialTicks)
}
private getTemperature (x: number, z: number): number {
return this.temperature
}
private getSkyColorByTemp (temperature: number): number {
temperature /= 3
if (temperature < -1) temperature = -1
if (temperature > 1) temperature = 1
// Apply debug fog orangeness to hue - positive values make it more orange, negative make it less orange
const baseHue = 0.622_222_2 - temperature * 0.05
// Orange is around hue 0.08-0.15, so we need to shift from blue-purple (0.62) toward orange
// Use a more dramatic shift and also increase saturation for more noticeable effect
const orangeHue = 0.12 // Orange hue value
const hue = this.fogOrangeness > 0
? baseHue + (orangeHue - baseHue) * this.fogOrangeness * 0.8 // Blend toward orange
: baseHue + this.fogOrangeness * 0.1 // Subtle shift for negative values
const saturation = 0.5 + temperature * 0.1 + Math.abs(this.fogOrangeness) * 0.3 // Increase saturation with orangeness
const brightness = 1
return this.hsbToRgb(hue, saturation, brightness)
}
private hsbToRgb (hue: number, saturation: number, brightness: number): number {
let r = 0; let g = 0; let b = 0
if (saturation === 0) {
r = g = b = Math.floor(brightness * 255 + 0.5)
} else {
const h = (hue - Math.floor(hue)) * 6
const f = h - Math.floor(h)
const p = brightness * (1 - saturation)
const q = brightness * (1 - saturation * f)
const t = brightness * (1 - (saturation * (1 - f)))
switch (Math.floor(h)) {
case 0:
r = Math.floor(brightness * 255 + 0.5)
g = Math.floor(t * 255 + 0.5)
b = Math.floor(p * 255 + 0.5)
break
case 1:
r = Math.floor(q * 255 + 0.5)
g = Math.floor(brightness * 255 + 0.5)
b = Math.floor(p * 255 + 0.5)
break
case 2:
r = Math.floor(p * 255 + 0.5)
g = Math.floor(brightness * 255 + 0.5)
b = Math.floor(t * 255 + 0.5)
break
case 3:
r = Math.floor(p * 255 + 0.5)
g = Math.floor(q * 255 + 0.5)
b = Math.floor(brightness * 255 + 0.5)
break
case 4:
r = Math.floor(t * 255 + 0.5)
g = Math.floor(p * 255 + 0.5)
b = Math.floor(brightness * 255 + 0.5)
break
case 5:
r = Math.floor(brightness * 255 + 0.5)
g = Math.floor(p * 255 + 0.5)
b = Math.floor(q * 255 + 0.5)
break
}
}
return 0xff_00_00_00 | (r << 16) | (g << 8) | (Math.trunc(b))
}
private updateSkyColors () {
if (!this.skyMesh || !this.voidMesh) return
// If default skybox is disabled, hide the skybox meshes
if (!this.defaultSkybox) {
this.skyMesh.visible = false
this.voidMesh.visible = false
if (this.mesh) {
this.mesh.visible = false
}
return
}
// Show skybox meshes when default skybox is enabled
this.skyMesh.visible = true
this.voidMesh.visible = true
if (this.mesh) {
this.mesh.visible = true
}
// Update fog brightness with smooth transition
this.prevFogBrightness = this.fogBrightness
const renderDistance = this.viewDistance / 32
const targetBrightness = this.brightnessAtPosition * (1 - renderDistance) + renderDistance
this.fogBrightness += (targetBrightness - this.fogBrightness) * 0.1
// Handle water fog
if (this.inWater) {
const waterViewDistance = this.waterBreathing ? 100 : 5
this.scene.fog = new THREE.Fog(new THREE.Color(0, 0, 1), 0.0025, waterViewDistance)
this.scene.background = new THREE.Color(0, 0, 1)
// Update sky and void colors for underwater effect
;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 1))
;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 0.6))
return
}
// Normal sky colors
const viewDistance = this.viewDistance * 16
const viewFactor = 1 - (0.25 + 0.75 * this.viewDistance / 32) ** 0.25
const angle = this.getCelestialAngle(this.partialTicks)
const skyColor = this.getSkyColor(0, 0, this.partialTicks)
const fogColor = this.getFogColor(this.partialTicks)
const brightness = Math.cos(angle * Math.PI * 2) * 2 + 0.5
const clampedBrightness = Math.max(0, Math.min(1, brightness))
// Interpolate fog brightness
const interpolatedBrightness = this.prevFogBrightness + (this.fogBrightness - this.prevFogBrightness) * this.partialTicks
const red = (fogColor.x + (skyColor.x - fogColor.x) * viewFactor) * clampedBrightness * interpolatedBrightness
const green = (fogColor.y + (skyColor.y - fogColor.y) * viewFactor) * clampedBrightness * interpolatedBrightness
const blue = (fogColor.z + (skyColor.z - fogColor.z) * viewFactor) * clampedBrightness * interpolatedBrightness
this.scene.background = new THREE.Color(red, green, blue)
this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * this.distanceFactor)
;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(skyColor.x, skyColor.y, skyColor.z))
;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(
skyColor.x * 0.2 + 0.04,
skyColor.y * 0.2 + 0.04,
skyColor.z * 0.6 + 0.1
))
}
dispose () {
@ -73,5 +392,15 @@ export class SkyboxRenderer {
;(this.mesh.material as THREE.Material).dispose()
this.scene.remove(this.mesh)
}
if (this.skyMesh) {
this.skyMesh.geometry.dispose()
;(this.skyMesh.material as THREE.Material).dispose()
this.scene.remove(this.skyMesh)
}
if (this.voidMesh) {
this.voidMesh.geometry.dispose()
;(this.voidMesh.material as THREE.Material).dispose()
this.scene.remove(this.voidMesh)
}
}
}

View file

@ -16,7 +16,7 @@ export const WAYPOINT_CONFIG = {
CANVAS_SCALE: 2,
ARROW: {
enabledDefault: false,
pixelSize: 30,
pixelSize: 50,
paddingPx: 50,
},
}
@ -50,6 +50,7 @@ export function createWaypointSprite (options: {
depthTest?: boolean,
// Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this)
labelYOffset?: number,
metadata?: any,
}): WaypointSprite {
const color = options.color ?? 0xFF_00_00
const depthTest = options.depthTest ?? false
@ -131,16 +132,22 @@ export function createWaypointSprite (options: {
canvas.height = size
const ctx = canvas.getContext('2d')!
ctx.clearRect(0, 0, size, size)
// Draw arrow shape
ctx.beginPath()
ctx.moveTo(size * 0.2, size * 0.5)
ctx.lineTo(size * 0.8, size * 0.5)
ctx.lineTo(size * 0.5, size * 0.2)
ctx.moveTo(size * 0.15, size * 0.5)
ctx.lineTo(size * 0.85, size * 0.5)
ctx.lineTo(size * 0.5, size * 0.15)
ctx.closePath()
ctx.lineWidth = 4
// Use waypoint color for arrow
const colorHex = `#${color.toString(16).padStart(6, '0')}`
ctx.lineWidth = 6
ctx.strokeStyle = 'black'
ctx.stroke()
ctx.fillStyle = 'white'
ctx.fillStyle = colorHex
ctx.fill()
const texture = new THREE.CanvasTexture(canvas)
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false })
arrowSprite = new THREE.Sprite(material)
@ -169,6 +176,9 @@ export function createWaypointSprite (options: {
ensureArrow()
if (!arrowSprite) return true
// Check if onlyLeftRight is enabled in metadata
const onlyLeftRight = options.metadata?.onlyLeftRight === true
// Build camera basis using camera.up to respect custom orientations
const forward = new THREE.Vector3()
camera.getWorldDirection(forward) // camera look direction
@ -213,6 +223,20 @@ export function createWaypointSprite (options: {
}
}
// Apply onlyLeftRight logic - restrict arrows to left/right edges only
if (onlyLeftRight) {
// Force the arrow to appear only on left or right edges
if (Math.abs(rx) > Math.abs(ry)) {
// Horizontal direction is dominant, keep it
ry = 0
} else {
// Vertical direction is dominant, but we want only left/right
// So choose left or right based on the sign of rx
rx = rx >= 0 ? 1 : -1
ry = 0
}
}
// Place on the rectangle border [-1,1]x[-1,1]
const s = Math.max(Math.abs(rx), Math.abs(ry)) || 1
let ndcX = rx / s

View file

@ -17,6 +17,7 @@ interface WaypointOptions {
color?: number
label?: string
minDistance?: number
metadata?: any
}
export class WaypointsRenderer {
@ -71,13 +72,14 @@ export class WaypointsRenderer {
this.removeWaypoint(id)
const color = options.color ?? 0xFF_00_00
const { label } = options
const { label, metadata } = options
const minDistance = options.minDistance ?? 0
const sprite = createWaypointSprite({
position: new THREE.Vector3(x, y, z),
color,
label: (label || id),
metadata,
})
sprite.enableOffscreenArrow(true)
sprite.setArrowParent(this.waypointScene)

View file

@ -28,7 +28,7 @@ export class CursorBlock {
}
cursorLineMaterial: LineMaterial
interactionLines: null | { blockPos: Vec3, mesh: THREE.Group } = null
interactionLines: null | { blockPos: Vec3, mesh: THREE.Group, shapePositions: BlocksShapes | undefined } = null
prevColor: string | undefined
blockBreakMesh: THREE.Mesh
breakTextures: THREE.Texture[] = []
@ -62,6 +62,13 @@ export class CursorBlock {
this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => {
this.updateLineMaterial()
})
// todo figure out why otherwise fog from skybox breaks it
setTimeout(() => {
this.updateLineMaterial()
if (this.interactionLines) {
this.setHighlightCursorBlock(this.interactionLines.blockPos, this.interactionLines.shapePositions, true)
}
})
}
// Update functions
@ -69,6 +76,9 @@ export class CursorBlock {
const inCreative = this.worldRenderer.playerStateReactive.gameMode === 'creative'
const pixelRatio = this.worldRenderer.renderer.getPixelRatio()
if (this.cursorLineMaterial) {
this.cursorLineMaterial.dispose()
}
this.cursorLineMaterial = new LineMaterial({
color: (() => {
switch (this.worldRenderer.worldRendererConfig.highlightBlockColor) {
@ -115,8 +125,8 @@ export class CursorBlock {
}
}
setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes): void {
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes, force = false): void {
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos) && !force) {
return
}
if (this.interactionLines !== null) {
@ -140,7 +150,7 @@ export class CursorBlock {
}
this.worldRenderer.scene.add(group)
group.visible = !this.cursorLinesHidden
this.interactionLines = { blockPos, mesh: group }
this.interactionLines = { blockPos, mesh: group, shapePositions }
}
render () {

View file

@ -3,6 +3,7 @@ import { Vec3 } from 'vec3'
import nbt from 'prismarine-nbt'
import PrismarineChatLoader from 'prismarine-chat'
import * as tweenJs from '@tweenjs/tween.js'
import { Biome } from 'minecraft-data'
import { renderSign } from '../sign-renderer'
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
import { chunkPos, sectionPos } from '../lib/simpleUtils'
@ -24,7 +25,7 @@ import { CameraShake } from './cameraShake'
import { ThreeJsMedia } from './threeJsMedia'
import { Fountain } from './threeJsParticles'
import { WaypointsRenderer } from './waypoints'
import { SkyboxRenderer } from './skyboxRenderer'
import { DEFAULT_TEMPERATURE, SkyboxRenderer } from './skyboxRenderer'
type SectionKey = string
@ -97,7 +98,7 @@ export class WorldRendererThree extends WorldRendererCommon {
this.holdingBlockLeft = new HoldingBlock(this, true)
// Initialize skybox renderer
this.skyboxRenderer = new SkyboxRenderer(this.scene, null)
this.skyboxRenderer = new SkyboxRenderer(this.scene, this.worldRendererConfig.defaultSkybox, null)
void this.skyboxRenderer.init()
this.addDebugOverlay()
@ -173,7 +174,10 @@ export class WorldRendererThree extends WorldRendererCommon {
override watchReactivePlayerState () {
super.watchReactivePlayerState()
this.onReactivePlayerStateUpdated('inWater', (value) => {
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null
this.skyboxRenderer.updateWaterState(value, this.playerStateReactive.waterBreathing)
})
this.onReactivePlayerStateUpdated('waterBreathing', (value) => {
this.skyboxRenderer.updateWaterState(this.playerStateReactive.inWater, value)
})
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
if (!value) return
@ -202,6 +206,9 @@ export class WorldRendererThree extends WorldRendererCommon {
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
this.updateShowChunksBorder(value)
})
this.onReactiveConfigUpdated('defaultSkybox', (value) => {
this.skyboxRenderer.updateDefaultSkybox(value)
})
}
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
@ -264,6 +271,19 @@ export class WorldRendererThree extends WorldRendererCommon {
} else {
this.starField.remove()
}
this.skyboxRenderer.updateTime(newTime)
}
biomeUpdated (biome: Biome): void {
if (biome?.temperature !== undefined) {
this.skyboxRenderer.updateTemperature(biome.temperature)
}
}
biomeReset (): void {
// Reset to default temperature when biome is unknown
this.skyboxRenderer.updateTemperature(DEFAULT_TEMPERATURE)
}
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
@ -716,7 +736,7 @@ export class WorldRendererThree extends WorldRendererCommon {
// Update skybox position to follow camera
const cameraPos = this.getCameraPosition()
this.skyboxRenderer.update(cameraPos)
this.skyboxRenderer.update(cameraPos, this.viewDistance)
const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov
if (sizeOrFovChanged) {
@ -767,12 +787,17 @@ export class WorldRendererThree extends WorldRendererCommon {
}
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
const textures = blockEntity.SkullOwner?.Properties?.textures[0]
if (!textures) return
let textureData: string
if (blockEntity.SkullOwner) {
textureData = blockEntity.SkullOwner.Properties?.textures?.[0]?.Value
} else {
textureData = blockEntity.profile?.properties?.find(p => p.name === 'textures')?.value
}
if (!textureData) return
try {
const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
let skinUrl = textureData.textures?.SKIN?.url
const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString())
let skinUrl = decodedData.textures?.SKIN?.url
const { skinTexturesProxy } = this.worldRendererConfig
if (skinTexturesProxy) {
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)

View file

@ -371,6 +371,7 @@ console.log('size', fs.lstatSync(filePath).size / 1000 / 1000, gzipSizeFromFileS
const { defaultVersion } = MCProtocol
const data = MinecraftData(defaultVersion)
console.log('defaultVersion', defaultVersion, !!data)
const initialMcData = {
[defaultVersion]: {
version: data.version,

View file

@ -35,7 +35,7 @@ export type AppConfig = {
// defaultVersion?: string
peerJsServer?: string
peerJsServerFallback?: string
promoteServers?: Array<{ ip, description, version? }>
promoteServers?: Array<{ ip, description, name?, version?, }>
mapsProvider?: string
appParams?: Record<string, any> // query string params

View file

@ -47,6 +47,7 @@ export type AppQsParams = {
connectText?: string
freezeSettings?: string
testIosCrash?: string
addPing?: string
// Replay params
replayFilter?: string

View file

@ -7,7 +7,12 @@ let audioContext: AudioContext
const sounds: Record<string, any> = {}
// Track currently playing sounds and their gain nodes
const activeSounds: Array<{ source: AudioBufferSourceNode; gainNode: GainNode; volumeMultiplier: number }> = []
const activeSounds: Array<{
source: AudioBufferSourceNode;
gainNode: GainNode;
volumeMultiplier: number;
isMusic: boolean;
}> = []
window.activeSounds = activeSounds
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
@ -43,7 +48,7 @@ export async function loadSound (path: string, contents = path) {
}
}
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false) => {
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false, isMusic = false) => {
const soundBuffer = sounds[url]
if (!soundBuffer) {
const start = Date.now()
@ -51,11 +56,11 @@ export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = option
if (cancelled || Date.now() - start > loadTimeout) return
}
return playSound(url, soundVolume, loop)
return playSound(url, soundVolume, loop, isMusic)
}
export async function playSound (url, soundVolume = 1, loop = false) {
const volume = soundVolume * (options.volume / 100)
export async function playSound (url, soundVolume = 1, loop = false, isMusic = false) {
const volume = soundVolume * (options.volume / 100) * (isMusic ? options.musicVolume / 100 : 1)
if (!volume) return
@ -82,7 +87,7 @@ export async function playSound (url, soundVolume = 1, loop = false) {
source.start(0)
// Add to active sounds
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume })
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume, isMusic })
const callbacks = [] as Array<() => void>
source.onended = () => {
@ -110,6 +115,7 @@ export async function playSound (url, soundVolume = 1, loop = false) {
console.warn('Failed to stop sound:', err)
}
},
gainNode,
}
}
@ -137,11 +143,11 @@ export function stopSound (url: string) {
}
}
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number, newMusicVolume: number) {
const normalizedVolume = newVolume / 100
for (const { gainNode, volumeMultiplier } of activeSounds) {
for (const { gainNode, volumeMultiplier, isMusic } of activeSounds) {
try {
gainNode.gain.value = normalizedVolume * volumeMultiplier
gainNode.gain.value = normalizedVolume * volumeMultiplier * (isMusic ? newMusicVolume / 100 : 1)
} catch (err) {
console.warn('Failed to change sound volume:', err)
}
@ -149,5 +155,9 @@ export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
}
subscribeKey(options, 'volume', () => {
changeVolumeOfCurrentlyPlayingSounds(options.volume)
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
})
subscribeKey(options, 'musicVolume', () => {
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
})

View file

@ -118,6 +118,14 @@ export const formatMessage = (message: MessageInput, mcData: IndexedData = globa
return msglist
}
export const messageToString = (message: MessageInput | string) => {
if (typeof message === 'string') {
return message
}
const msglist = formatMessage(message)
return msglist.map(msg => msg.text).join('')
}
const blockToItemRemaps = {
water: 'water_bucket',
lava: 'lava_bucket',

View file

@ -7,18 +7,15 @@ import { registerIdeChannels } from './core/ideChannels'
export default () => {
customEvents.on('mineflayerBotCreated', async () => {
if (!options.customChannels) return
await new Promise(resolve => {
bot.once('login', () => {
resolve(true)
})
bot.once('login', () => {
registerBlockModelsChannel()
registerMediaChannels()
registerSectionAnimationChannels()
registeredJeiChannel()
registerBlockInteractionsCustomizationChannel()
registerWaypointChannels()
registerIdeChannels()
})
registerBlockModelsChannel()
registerMediaChannels()
registerSectionAnimationChannels()
registeredJeiChannel()
registerBlockInteractionsCustomizationChannel()
registerWaypointChannels()
registerIdeChannels()
})
}
@ -50,19 +47,7 @@ const registerBlockInteractionsCustomizationChannel = () => {
registerChannel(CHANNEL_NAME, packetStructure, (data) => {
const config = JSON.parse(data.newConfiguration)
if (config.customBreakTime !== undefined && Object.values(config.customBreakTime).every(x => typeof x === 'number')) {
bot.mouse.customBreakTime = config.customBreakTime
}
if (config.customBreakTimeToolAllowance !== undefined) {
bot.mouse.customBreakTimeToolAllowance = new Set(config.customBreakTimeToolAllowance)
}
if (config.blockPlacePrediction !== undefined) {
bot.mouse.settings.blockPlacePrediction = config.blockPlacePrediction
}
if (config.blockPlacePredictionDelay !== undefined) {
bot.mouse.settings.blockPlacePredictionDelay = config.blockPlacePredictionDelay
}
bot.mouse.setConfigFromPacket(config)
}, true)
}
@ -97,15 +82,30 @@ const registerWaypointChannels = () => {
{
name: 'color',
type: 'i32'
},
{
name: 'metadataJson',
type: ['pstring', { countType: 'i16' }]
}
]
]
registerChannel('minecraft-web-client:waypoint-add', packetStructure, (data) => {
// Parse metadata if provided
let metadata: any = {}
if (data.metadataJson && data.metadataJson.trim() !== '') {
try {
metadata = JSON.parse(data.metadataJson)
} catch (error) {
console.warn('Failed to parse waypoint metadataJson:', error)
}
}
getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, {
minDistance: data.minDistance,
label: data.label || undefined,
color: data.color || undefined
color: data.color || undefined,
metadata
})
})

View file

@ -1,46 +0,0 @@
import { options } from './optionsStorage'
import { assertDefined } from './utils'
import { updateBackground } from './water'
export default () => {
const timeUpdated = () => {
// 0 morning
const dayTotal = 24_000
const evening = 11_500
const night = 13_500
const morningStart = 23_000
const morningEnd = 23_961
const timeProgress = options.dayCycleAndLighting ? bot.time.timeOfDay : 0
// todo check actual colors
const dayColorRainy = { r: 111 / 255, g: 156 / 255, b: 236 / 255 }
// todo yes, we should make animations (and rain)
// eslint-disable-next-line unicorn/numeric-separators-style
const dayColor = bot.isRaining ? dayColorRainy : { r: 0.6784313725490196, g: 0.8470588235294118, b: 0.9019607843137255 } // lightblue
// let newColor = dayColor
let int = 1
if (timeProgress < evening) {
// stay dayily
} else if (timeProgress < night) {
const progressNorm = timeProgress - evening
const progressMax = night - evening
int = 1 - progressNorm / progressMax
} else if (timeProgress < morningStart) {
int = 0
} else if (timeProgress < morningEnd) {
const progressNorm = timeProgress - morningStart
const progressMax = night - morningEnd
int = progressNorm / progressMax
}
// todo need to think wisely how to set these values & also move directional light around!
const colorInt = Math.max(int, 0.1)
updateBackground({ r: dayColor.r * colorInt, g: dayColor.g * colorInt, b: dayColor.b * colorInt })
if (!options.newVersionsLighting && bot.supportFeature('blockStateId')) {
appViewer.playerState.reactive.ambientLight = Math.max(int, 0.25)
appViewer.playerState.reactive.directionalLight = Math.min(int, 0.5)
}
}
bot.on('time', timeUpdated)
timeUpdated()
}

View file

@ -16,7 +16,8 @@ export const defaultOptions = {
chatOpacityOpened: 100,
messagesLimit: 200,
volume: 50,
enableMusic: false,
enableMusic: true,
musicVolume: 50,
// fov: 70,
fov: 75,
defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front',
@ -41,6 +42,7 @@ export const defaultOptions = {
renderEars: true,
lowMemoryMode: false,
starfieldRendering: true,
defaultSkybox: true,
enabledResourcepack: null as string | null,
useVersionsTextures: 'latest',
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
@ -83,6 +85,7 @@ export const defaultOptions = {
localServerOptions: {
gameMode: 1
} as any,
saveLoginPassword: 'prompt' as 'prompt' | 'never' | 'always',
preferLoadReadonly: false,
experimentalClientSelfReload: false,
remoteSoundsSupport: false,

View file

@ -5,6 +5,17 @@ import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree'
import { enable, disable, enabled } from 'debug'
import { Vec3 } from 'vec3'
customEvents.on('mineflayerBotCreated', () => {
window.debugServerPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toClient.types).map(name => {
name = name.replace('packet_', '')
return [name, name]
}))
window.debugClientPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toServer.types).map(name => {
name = name.replace('packet_', '')
return [name, name]
}))
})
window.Vec3 = Vec3
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)

View file

@ -246,22 +246,29 @@ customEvents.on('gameLoaded', () => {
}
}
// even if not found, still record to cache
void getThreeJsRendererMethods()?.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
void getThreeJsRendererMethods()!.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
} catch (err) {
console.error('Error decoding player texture:', err)
reportError(new Error('Error applying skin texture:', { cause: err }))
}
}
bot.on('playerJoined', updateSkin)
bot.on('playerUpdated', updateSkin)
for (const entity of Object.values(bot.players)) {
updateSkin(entity)
}
bot.on('teamUpdated', (team: Team) => {
const teamUpdated = (team: Team) => {
for (const entity of Object.values(bot.entities)) {
if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) {
bot.emit('entityUpdate', entity)
}
}
})
}
bot.on('teamUpdated', teamUpdated)
for (const team of Object.values(bot.teams)) {
teamUpdated(team)
}
const updateEntityNameTags = (team: Team) => {
for (const entity of Object.values(bot.entities)) {

View file

@ -56,7 +56,6 @@ import { isCypress } from './standaloneUtils'
import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
import defaultServerOptions from './defaultLocalServerOptions'
import dayCycle from './dayCycle'
import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack'
import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer'
@ -305,7 +304,7 @@ export async function connect (connectOptions: ConnectOptions) {
if (connectOptions.server && !connectOptions.viewerWsConnect && !parsedServer.isWebSocket) {
console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`)
net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` } })
net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` }, artificialDelay: appQueryParams.addPing ? Number(appQueryParams.addPing) : undefined })
}
const renderDistance = singleplayer ? renderDistanceSingleplayer : multiplayerRenderDistance
@ -794,7 +793,6 @@ export async function connect (connectOptions: ConnectOptions) {
}
initMotionTracking()
dayCycle()
// Bot position callback
const botPosition = () => {

View file

@ -12,6 +12,7 @@ import PrismarineChatLoader from 'prismarine-chat'
import * as nbt from 'prismarine-nbt'
import { BlockModel } from 'mc-assets'
import { renderSlot } from 'renderer/viewer/three/renderSlot'
import { loadSkinFromUsername } from 'renderer/viewer/lib/utils/skins'
import Generic95 from '../assets/generic_95.png'
import { appReplacableResources } from './generated/resources'
import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState'
@ -23,6 +24,7 @@ import { getItemDescription } from './itemsDescriptions'
import { MessageFormatPart } from './chatUtils'
import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items'
import { playerState } from './mineflayer/playerState'
import { modelViewerState } from './react/OverlayModelViewer'
const loadedImagesCache = new Map<string, HTMLImageElement | ImageBitmap>()
const cleanLoadedImagesCache = () => {
@ -40,6 +42,34 @@ export const jeiCustomCategories = proxy({
value: [] as Array<{ id: string, categoryTitle: string, items: any[] }>
})
let remotePlayerSkin: string | undefined | Promise<string>
export const showInventoryPlayer = () => {
modelViewerState.model = {
positioning: {
windowWidth: 176,
windowHeight: 166,
x: 25,
y: 8,
width: 50,
height: 70,
scaled: true,
onlyInitialScale: true,
followCursor: true,
},
// models: ['https://bucket.mcraft.fun/sitarbuckss.glb'],
// debug: true,
steveModelSkin: appViewer.playerState.reactive.playerSkin ?? (typeof remotePlayerSkin === 'string' ? remotePlayerSkin : ''),
}
if (remotePlayerSkin === undefined && !appViewer.playerState.reactive.playerSkin) {
remotePlayerSkin = loadSkinFromUsername(bot.username, 'skin').then(a => {
setTimeout(() => { showInventoryPlayer() }, 0) // todo patch instead and make reactive
remotePlayerSkin = a ?? ''
return remotePlayerSkin
})
}
}
export const onGameLoad = () => {
version = bot.version
@ -57,12 +87,23 @@ export const onGameLoad = () => {
return type
}
const maybeParseNbtJson = (data: any) => {
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (err) {
// ignore
}
}
return nbt.simplify(data) ?? data
}
bot.on('windowOpen', (win) => {
const implementedWindow = implementedContainersGuiMap[mapWindowType(win.type as string, win.inventoryStart)]
if (implementedWindow) {
openWindow(implementedWindow, nbt.simplify(win.title as any))
openWindow(implementedWindow, maybeParseNbtJson(win.title))
} else if (options.unimplementedContainers) {
openWindow('ChestWin', nbt.simplify(win.title as any))
openWindow('ChestWin', maybeParseNbtJson(win.title))
} else {
// todo format
displayClientChat(`[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}`)
@ -381,7 +422,12 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
miscUiState.displaySearchInput = false
destroyFn()
skipClosePacketSending = false
modelViewerState.model = undefined
})
if (type === undefined) {
showInventoryPlayer()
}
cleanLoadedImagesCache()
const inv = openItemsCanvas(type)
inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch
@ -424,6 +470,7 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
const isRightClick = type === 'rightclick'
const isLeftClick = type === 'leftclick'
if (isLeftClick || isRightClick) {
modelViewerState.model = undefined
inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item)
}
} else {
@ -455,6 +502,7 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
if (freeSlot === null) return
void bot.creative.setInventorySlot(freeSlot, item)
} else {
modelViewerState.model = undefined
inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0])
}
}

View file

@ -130,7 +130,8 @@ export const setProxy = (proxyParams: ProxyParams) => {
net['setProxy']({
hostname: proxy.host,
port: proxy.port,
headers: proxyParams.headers
headers: proxyParams.headers,
artificialDelay: appQueryParams.addPing ? Number(appQueryParams.addPing) : undefined
})
return {
proxy

View file

@ -110,7 +110,7 @@ const domListeners = (bot: Bot) => {
}, { signal: abortController.signal })
bot.mouse.beforeUpdateChecks = () => {
if (!document.hasFocus()) {
if (!document.hasFocus() || !isGameActive(true)) {
// deactive all buttons
bot.mouse.buttons.fill(false)
}

View file

@ -15,9 +15,12 @@ class CustomDuplex extends Duplex {
}
export const getWebsocketStream = async (host: string) => {
const baseProtocol = location.protocol === 'https:' ? 'wss' : host.startsWith('ws://') ? 'ws' : 'wss'
const baseProtocol = host.startsWith('ws://') ? 'ws' : 'wss'
const hostClean = host.replace('ws://', '').replace('wss://', '')
const ws = new WebSocket(`${baseProtocol}://${hostClean}`)
const hostURL = new URL(`${baseProtocol}://${hostClean}`)
const hostParams = hostURL.searchParams
hostParams.append('client_mcraft', '')
const ws = new WebSocket(`${baseProtocol}://${hostURL.host}${hostURL.pathname}?${hostParams.toString()}`)
const clientDuplex = new CustomDuplex(undefined, data => {
ws.send(data)
})

View file

@ -480,6 +480,24 @@ export const guiOptionsScheme: {
],
sound: [
{ volume: {} },
{
custom () {
return <OptionSlider
valueOverride={options.enableMusic ? undefined : 0}
onChange={(value) => {
options.musicVolume = value
}}
item={{
type: 'slider',
id: 'musicVolume',
text: 'Music Volume',
min: 0,
max: 100,
unit: '%',
}}
/>
},
},
{
custom () {
return <Button label='Sound Muffler' onClick={() => showModal({ reactType: 'sound-muffler' })} inScreen />
@ -550,6 +568,16 @@ export const guiOptionsScheme: {
return <Category>Server Connection</Category>
},
},
{
saveLoginPassword: {
tooltip: 'Controls whether to save login passwords for servers in this browser memory.',
values: [
'prompt',
'always',
'never'
]
},
},
{
custom () {
const { serversAutoVersionSelect } = useSnapshot(options)

View file

@ -59,6 +59,7 @@ export const startLocalReplayServer = (contents: string) => {
const server = createServer({
Server: LocalServer as any,
version: header.minecraftVersion,
keepAlive: false,
'online-mode': false
})

View file

@ -117,7 +117,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
}
const displayConnectButton = qsParamIp
const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg']
const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg', 'wss://play.webmc.fun']
// pick random example
const example = serverExamples[Math.floor(Math.random() * serverExamples.length)]

View file

@ -125,7 +125,9 @@ export default ({
const chatInput = useRef<HTMLInputElement>(null!)
const chatMessages = useRef<HTMLDivElement>(null)
const chatHistoryPos = useRef(sendHistoryRef.current.length)
const commandHistoryPos = useRef(0)
const inputCurrentlyEnteredValue = useRef('')
const commandHistoryRef = useRef(sendHistoryRef.current.filter((msg: string) => msg.startsWith('/')))
const { scrollToBottom, isAtBottom, wasAtBottom, currentlyAtBottom } = useScrollBehavior(chatMessages, { messages, opened })
const [rightNowAtBottom, setRightNowAtBottom] = useState(false)
@ -142,6 +144,9 @@ export default ({
sendHistoryRef.current = newHistory
window.sessionStorage.chatHistory = JSON.stringify(newHistory)
chatHistoryPos.current = newHistory.length
// Update command history (only messages starting with /)
commandHistoryRef.current = newHistory.filter((msg: string) => msg.startsWith('/'))
commandHistoryPos.current = commandHistoryRef.current.length
}
const acceptComplete = (item: string) => {
@ -180,6 +185,21 @@ export default ({
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
}
const handleCommandArrowUp = () => {
if (commandHistoryPos.current === 0 || commandHistoryRef.current.length === 0) return
if (commandHistoryPos.current === commandHistoryRef.current.length) { // started navigating command history
inputCurrentlyEnteredValue.current = chatInput.current.value
}
commandHistoryPos.current--
updateInputValue(commandHistoryRef.current[commandHistoryPos.current] || '')
}
const handleCommandArrowDown = () => {
if (commandHistoryPos.current === commandHistoryRef.current.length) return
commandHistoryPos.current++
updateInputValue(commandHistoryRef.current[commandHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
}
const auxInputFocus = (direction: 'up' | 'down') => {
chatInput.current.focus()
if (direction === 'up') {
@ -203,6 +223,7 @@ export default ({
updateInputValue(chatInputValueGlobal.value)
chatInputValueGlobal.value = ''
chatHistoryPos.current = sendHistoryRef.current.length
commandHistoryPos.current = commandHistoryRef.current.length
if (!usingTouch) {
chatInput.current.focus()
}
@ -524,9 +545,19 @@ export default ({
onBlur={() => setIsInputFocused(false)}
onKeyDown={(e) => {
if (e.code === 'ArrowUp') {
handleArrowUp()
if (e.altKey) {
handleCommandArrowUp()
e.preventDefault()
} else {
handleArrowUp()
}
} else if (e.code === 'ArrowDown') {
handleArrowDown()
if (e.altKey) {
handleCommandArrowDown()
e.preventDefault()
} else {
handleArrowDown()
}
}
if (e.code === 'Tab') {
if (completionItemsSource.length) {

View file

@ -73,16 +73,28 @@ export default () => {
}
const builtinHandled = tryHandleBuiltinCommand(message)
if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register'))) {
showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, () => {
if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register')) && options.saveLoginPassword !== 'never') {
const savePassword = () => {
let hadPassword = false
updateLoadedServerData((server) => {
server.autoLogin ??= {}
const password = message.split(' ')[1]
hadPassword = !!server.autoLogin[bot.username]
server.autoLogin[bot.username] = password
return { ...server }
})
hideNotification()
})
if (options.saveLoginPassword === 'always') {
const message = hadPassword ? 'Password updated in browser for auto-login' : 'Password saved in browser for auto-login'
showNotification(message, undefined, false, undefined)
} else {
hideNotification()
}
}
if (options.saveLoginPassword === 'prompt') {
showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, savePassword)
} else {
savePassword()
}
notificationProxy.id = 'auto-login'
const listener = () => {
hideNotification()

View file

@ -161,7 +161,15 @@ export const OptionButton = ({ item, onClick, valueText, cacheKey }: {
/>
}
export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slider' }> }) => {
export const OptionSlider = ({
item,
onChange,
valueOverride
}: {
item: Extract<OptionMeta, { type: 'slider' }>
onChange?: (value: number) => void
valueOverride?: number
}) => {
const { disabledBecauseOfSetting } = useCommonComponentsProps(item)
const optionValue = useSnapshot(options)[item.id!]
@ -174,7 +182,7 @@ export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slid
return (
<Slider
label={item.text!}
value={options[item.id!]}
value={valueOverride ?? options[item.id!]}
data-setting={item.id}
disabledReason={isLocked(item) ? 'qs' : disabledBecauseOfSetting ? `Disabled because ${item.disableIf![0]} is ${item.disableIf![1]}` : item.disabledReason}
min={item.min}
@ -184,6 +192,7 @@ export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slid
updateOnDragEnd={item.delayApply}
updateValue={(value) => {
options[item.id!] = value
onChange?.(value)
}}
/>
)

View file

@ -0,0 +1,554 @@
import { proxy, useSnapshot, subscribe } from 'valtio'
import { useEffect, useMemo, useRef } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { applySkinToPlayerObject, createPlayerObject, PlayerObjectType } from '../../renderer/viewer/lib/createPlayerObject'
import { currentScaling } from '../scaleInterface'
import { activeModalStack } from '../globalState'
THREE.ColorManagement.enabled = false
export const modelViewerState = proxy({
model: undefined as undefined | {
models?: string[] // Array of model URLs (URL itself is the cache key)
steveModelSkin?: string
debug?: boolean
// absolute positioning
positioning: {
windowWidth: number
windowHeight: number
x: number
y: number
width: number
height: number
scaled?: boolean
onlyInitialScale?: boolean
followCursor?: boolean
}
modelCustomization?: { [modelUrl: string]: { color?: string, opacity?: number, metalness?: number, roughness?: number } }
resetRotationOnReleae?: boolean
continiousRender?: boolean
alwaysRender?: boolean
}
})
globalThis.modelViewerState = modelViewerState
// Global debug function to get camera and model values
globalThis.getModelViewerValues = () => {
const scene = globalThis.sceneRef?.current
if (!scene) return null
const { camera, playerObject } = scene
if (!playerObject) return null
const wrapper = playerObject.parent
if (!wrapper) return null
const box = new THREE.Box3().setFromObject(wrapper)
const size = box.getSize(new THREE.Vector3())
const center = box.getCenter(new THREE.Vector3())
return {
camera: {
position: camera.position.clone(),
fov: camera.fov,
aspect: camera.aspect
},
model: {
position: wrapper.position.clone(),
rotation: wrapper.rotation.clone(),
scale: wrapper.scale.clone(),
size,
center
},
cursor: {
position: globalThis.cursorPosition || { x: 0, y: 0 },
normalized: globalThis.cursorPosition ? {
x: globalThis.cursorPosition.x * 2 - 1,
y: globalThis.cursorPosition.y * 2 - 1
} : { x: 0, y: 0 }
},
visibleArea: {
height: 2 * Math.tan(camera.fov * Math.PI / 180 / 2) * camera.position.z,
width: 2 * Math.tan(camera.fov * Math.PI / 180 / 2) * camera.position.z * camera.aspect
}
}
}
subscribe(activeModalStack, () => {
if (!modelViewerState.model || !modelViewerState.model?.alwaysRender) {
return
}
if (activeModalStack.length === 0) {
modelViewerState.model = undefined
}
})
export default () => {
const { model } = useSnapshot(modelViewerState)
const containerRef = useRef<HTMLDivElement>(null)
const sceneRef = useRef<{
scene: THREE.Scene
camera: THREE.PerspectiveCamera
renderer: THREE.WebGLRenderer
controls: OrbitControls
playerObject?: PlayerObjectType
dispose: () => void
}>()
const initialScale = useMemo(() => {
return currentScaling.scale
}, [])
globalThis.sceneRef = sceneRef
// Cursor following state
const cursorPosition = useRef({ x: 0, y: 0 })
const isFollowingCursor = useRef(false)
// Model management state
const loadedModels = useRef<Map<string, THREE.Object3D>>(new Map())
const modelLoaders = useRef<Map<string, GLTFLoader | OBJLoader>>(new Map())
// Model management functions
const loadModel = (modelUrl: string) => {
if (loadedModels.current.has(modelUrl)) return // Already loaded
const isGLTF = modelUrl.toLowerCase().endsWith('.gltf') || modelUrl.toLowerCase().endsWith('.glb')
const loader = isGLTF ? new GLTFLoader() : new OBJLoader()
modelLoaders.current.set(modelUrl, loader)
const onLoad = (object: THREE.Object3D) => {
// Apply customization if available and enable shadows
const customization = model?.modelCustomization?.[modelUrl]
object.traverse((child) => {
if (child instanceof THREE.Mesh) {
// Enable shadow casting and receiving for all meshes
child.castShadow = true
child.receiveShadow = true
if (child.material && customization) {
const material = child.material as THREE.MeshStandardMaterial
if (customization.color) {
material.color.setHex(parseInt(customization.color.replace('#', ''), 16))
}
if (customization.opacity !== undefined) {
material.opacity = customization.opacity
material.transparent = customization.opacity < 1
}
if (customization.metalness !== undefined) {
material.metalness = customization.metalness
}
if (customization.roughness !== undefined) {
material.roughness = customization.roughness
}
}
}
})
// Center and scale model
const box = new THREE.Box3().setFromObject(object)
const center = box.getCenter(new THREE.Vector3())
const size = box.getSize(new THREE.Vector3())
const maxDim = Math.max(size.x, size.y, size.z)
const scale = 2 / maxDim
object.scale.setScalar(scale)
object.position.sub(center.multiplyScalar(scale))
// Store the model using URL as key
loadedModels.current.set(modelUrl, object)
sceneRef.current?.scene.add(object)
// Trigger render
if (sceneRef.current) {
setTimeout(() => {
const render = () => sceneRef.current?.renderer.render(sceneRef.current.scene, sceneRef.current.camera)
render()
}, 0)
}
}
if (isGLTF) {
(loader as GLTFLoader).load(modelUrl, (gltf) => {
onLoad(gltf.scene)
})
} else {
(loader as OBJLoader).load(modelUrl, onLoad)
}
}
const removeModel = (modelUrl: string) => {
const model = loadedModels.current.get(modelUrl)
if (model) {
sceneRef.current?.scene.remove(model)
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
if (child.material) {
if (Array.isArray(child.material)) {
for (const mat of child.material) {
mat.dispose()
}
} else {
child.material.dispose()
}
}
if (child.geometry) {
child.geometry.dispose()
}
}
})
loadedModels.current.delete(modelUrl)
}
modelLoaders.current.delete(modelUrl)
}
// Subscribe to model changes
useEffect(() => {
if (!modelViewerState.model?.models) return
const modelsChanged = () => {
const currentModels = modelViewerState.model?.models || []
const currentModelUrls = new Set(currentModels)
const loadedModelUrls = new Set(loadedModels.current.keys())
// Remove models that are no longer in the state
for (const modelUrl of loadedModelUrls) {
if (!currentModelUrls.has(modelUrl)) {
removeModel(modelUrl)
}
}
// Add new models
for (const modelUrl of currentModels) {
if (!loadedModelUrls.has(modelUrl)) {
loadModel(modelUrl)
}
}
}
const unsubscribe = subscribe(modelViewerState.model.models, modelsChanged)
let unmounted = false
setTimeout(() => {
if (unmounted) return
modelsChanged()
})
return () => {
unmounted = true
unsubscribe?.()
}
}, [model?.models])
useEffect(() => {
if (!model || !containerRef.current) return
// Setup scene
const scene = new THREE.Scene()
scene.background = null // Transparent background
// Setup camera with optimal settings for player model viewing
const camera = new THREE.PerspectiveCamera(
50, // Reduced FOV for better model viewing
model.positioning.width / model.positioning.height,
0.1,
1000
)
camera.position.set(0, 0, 3) // Position camera to view player model optimally
// Setup renderer with pixel density awareness
const renderer = new THREE.WebGLRenderer({ alpha: true })
let scale = window.devicePixelRatio || 1
if (modelViewerState.model?.positioning.scaled) {
scale *= currentScaling.scale
}
renderer.setPixelRatio(scale)
renderer.setSize(model.positioning.width, model.positioning.height)
// Enable shadow rendering for depth and realism
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap // Soft shadows for better quality
renderer.shadowMap.autoUpdate = true
containerRef.current.appendChild(renderer.domElement)
// Setup controls
const controls = new OrbitControls(camera, renderer.domElement)
// controls.enableZoom = false
// controls.enablePan = false
controls.minPolarAngle = Math.PI / 2 // Lock vertical rotation
controls.maxPolarAngle = Math.PI / 2
controls.enableDamping = true
controls.dampingFactor = 0.05
// Add ambient light for overall illumination
const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 0.4) // Reduced intensity to allow shadows
scene.add(ambientLight)
// Add directional light for shadows and depth (similar to Minecraft inventory lighting)
const directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.6)
directionalLight.position.set(2, 2, 2) // Position light from top-right-front
directionalLight.target.position.set(0, 0, 0) // Point towards center of scene
// Configure shadow properties for optimal quality
directionalLight.castShadow = true
directionalLight.shadow.mapSize.width = 2048 // High resolution shadow map
directionalLight.shadow.mapSize.height = 2048
directionalLight.shadow.camera.near = 0.1
directionalLight.shadow.camera.far = 10
directionalLight.shadow.camera.left = -3
directionalLight.shadow.camera.right = 3
directionalLight.shadow.camera.top = 3
directionalLight.shadow.camera.bottom = -3
directionalLight.shadow.bias = -0.0001 // Reduce shadow acne
scene.add(directionalLight)
scene.add(directionalLight.target)
// Cursor following function
const updatePlayerLookAt = () => {
if (!isFollowingCursor.current || !sceneRef.current?.playerObject) return
const { playerObject } = sceneRef.current
const { x, y } = cursorPosition.current
// Convert 0-1 cursor position to normalized coordinates (-1 to 1)
const normalizedX = x * 2 - 1
const normalizedY = y * 2 - 1 // Inverted: top of screen = negative pitch, bottom = positive pitch
// Calculate head rotation based on cursor position
// Limit head movement to realistic angles
const maxHeadYaw = Math.PI / 3 // 60 degrees
const maxHeadPitch = Math.PI / 4 // 45 degrees
const headYaw = normalizedX * maxHeadYaw
const headPitch = normalizedY * maxHeadPitch
// Apply head rotation with smooth interpolation
const lerpFactor = 0.1 // Smooth interpolation factor
playerObject.skin.head.rotation.y = THREE.MathUtils.lerp(
playerObject.skin.head.rotation.y,
headYaw,
lerpFactor
)
playerObject.skin.head.rotation.x = THREE.MathUtils.lerp(
playerObject.skin.head.rotation.x,
headPitch,
lerpFactor
)
// Apply slight body rotation for more natural movement
const bodyYaw = headYaw * 0.3 // Body follows head but with less rotation
playerObject.rotation.y = THREE.MathUtils.lerp(
playerObject.rotation.y,
bodyYaw,
lerpFactor * 0.5 // Slower body movement
)
render()
}
// Render function
const render = () => {
renderer.render(scene, camera)
}
// Setup animation/render strategy
if (model.continiousRender) {
// Continuous animation loop
const animate = () => {
requestAnimationFrame(animate)
render()
}
animate()
} else {
// Render only on camera movement
controls.addEventListener('change', render)
// Initial render
render()
// Render after model loads
if (model.steveModelSkin !== undefined) {
// Create player model
const { playerObject, wrapper } = createPlayerObject({
scale: 1 // Start with base scale, will adjust below
})
// Enable shadows for player object
wrapper.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = true
child.receiveShadow = true
}
})
// Calculate proper scale and positioning for camera view
const box = new THREE.Box3().setFromObject(wrapper)
const size = box.getSize(new THREE.Vector3())
const center = box.getCenter(new THREE.Vector3())
// Calculate scale to fit within camera view (considering FOV and distance)
const cameraDistance = camera.position.z
const fov = camera.fov * Math.PI / 180 // Convert to radians
const visibleHeight = 2 * Math.tan(fov / 2) * cameraDistance
const visibleWidth = visibleHeight * (model.positioning.width / model.positioning.height)
const scaleFactor = Math.min(
(visibleHeight) / size.y,
(visibleWidth) / size.x
)
wrapper.scale.multiplyScalar(scaleFactor)
// Center the player object
wrapper.position.sub(center.multiplyScalar(scaleFactor))
// Rotate to face camera (remove the default 180° rotation)
wrapper.rotation.set(0, 0, 0)
scene.add(wrapper)
sceneRef.current = {
...sceneRef.current!,
playerObject
}
void applySkinToPlayerObject(playerObject, model.steveModelSkin).then(() => {
setTimeout(render, 0)
})
// Set up cursor following if enabled
if (model.positioning.followCursor) {
isFollowingCursor.current = true
}
}
}
// Window cursor tracking for followCursor
let lastCursorUpdate = 0
let waitingRender = false
const handleWindowPointerMove = (event: PointerEvent) => {
if (!model.positioning.followCursor) return
// Track cursor position as 0-1 across the entire window
const newPosition = {
x: event.clientX / window.innerWidth,
y: event.clientY / window.innerHeight
}
cursorPosition.current = newPosition
globalThis.cursorPosition = newPosition // Expose for debug
lastCursorUpdate = Date.now()
updatePlayerLookAt()
if (!waitingRender) {
requestAnimationFrame(() => {
render()
waitingRender = false
})
waitingRender = true
}
}
// Add window event listeners
if (model.positioning.followCursor) {
window.addEventListener('pointermove', handleWindowPointerMove)
isFollowingCursor.current = true
}
// Store refs for cleanup
sceneRef.current = {
...sceneRef.current!,
scene,
camera,
renderer,
controls,
dispose () {
if (!model.continiousRender) {
controls.removeEventListener('change', render)
}
if (model.positioning.followCursor) {
window.removeEventListener('pointermove', handleWindowPointerMove)
}
// Clean up loaded models
for (const [modelUrl, model] of loadedModels.current) {
scene.remove(model)
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
if (child.material) {
if (Array.isArray(child.material)) {
for (const mat of child.material) {
mat.dispose()
}
} else {
child.material.dispose()
}
}
if (child.geometry) {
child.geometry.dispose()
}
}
})
}
loadedModels.current.clear()
modelLoaders.current.clear()
const playerObject = sceneRef.current?.playerObject
if (playerObject?.skin.map) {
(playerObject.skin.map as unknown as THREE.Texture).dispose()
}
renderer.dispose()
renderer.domElement?.remove()
}
}
return () => {
sceneRef.current?.dispose()
}
}, [model])
if (!model) return null
const { x, y, width, height, scaled, onlyInitialScale } = model.positioning
const { windowWidth } = model.positioning
const { windowHeight } = model.positioning
const scaleValue = onlyInitialScale ? initialScale : 'var(--guiScale)'
return (
<div
className='overlay-model-viewer-container'
style={{
zIndex: 100,
position: 'fixed',
inset: 0,
width: '100dvw',
height: '100dvh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transform: scaled ? `scale(${scaleValue})` : 'none',
pointerEvents: 'none',
}}
>
<div
className='overlay-model-viewer-window'
style={{
width: windowWidth,
height: windowHeight,
position: 'relative',
pointerEvents: 'none',
}}
>
<div
ref={containerRef}
className='overlay-model-viewer'
style={{
position: 'absolute',
left: x,
top: y,
width,
height,
pointerEvents: 'auto',
backgroundColor: model.debug ? 'red' : undefined,
}}
/>
</div>
</div>
)
}

View file

@ -119,6 +119,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
...serversListProvided,
...(customServersList ? [] : (miscUiState.appConfig?.promoteServers ?? [])).map((server): StoreServerItem => ({
ip: server.ip,
name: server.name,
versionOverride: server.version,
description: server.description,
isRecommended: true
@ -167,6 +168,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
console.log('pingResult.fullInfo.description', pingResult.fullInfo.description)
data = {
formattedText: pingResult.fullInfo.description,
icon: pingResult.fullInfo.favicon,
textNameRight: `ws ${pingResult.latency}ms`,
textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`,
offline: false

View file

@ -68,6 +68,7 @@ import FullscreenTime from './react/FullscreenTime'
import StorageConflictModal from './react/StorageConflictModal'
import FireRenderer from './react/FireRenderer'
import MonacoEditor from './react/MonacoEditor'
import OverlayModelViewer from './react/OverlayModelViewer'
const isFirefox = ua.getBrowser().name === 'Firefox'
if (isFirefox) {
@ -259,6 +260,7 @@ const App = () => {
</div>
<div />
<DebugEdges />
<OverlayModelViewer />
<MonacoEditor />
<DebugResponseTimeIndicator />
</RobustPortal>

View file

@ -486,17 +486,6 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres
}
}
const waitForGameEvent = async () => {
if (miscUiState.gameLoaded) return
await new Promise<void>(resolve => {
const listener = () => resolve()
customEvents.once('gameLoaded', listener)
watchUnloadForCleanup(() => {
customEvents.removeListener('gameLoaded', listener)
})
})
}
export const onAppLoad = () => {
customEvents.on('mineflayerBotCreated', () => {
// todo also handle resourcePack

View file

@ -26,6 +26,10 @@
display: flex;
justify-content: center;
z-index: 12;
/* Account for GUI scaling */
width: calc(100dvw / var(--guiScale, 1));
height: calc(100dvh / var(--guiScale, 1));
overflow: hidden;
}
.screen-content {

View file

@ -5,10 +5,10 @@ class MusicSystem {
private currentMusic: string | null = null
async playMusic (url: string, musicVolume = 1) {
if (!options.enableMusic || this.currentMusic) return
if (!options.enableMusic || this.currentMusic || options.musicVolume === 0) return
try {
const { onEnded } = await loadOrPlaySound(url, 0.5 * musicVolume, 5000) ?? {}
const { onEnded } = await loadOrPlaySound(url, musicVolume, 5000, undefined, true) ?? {}
if (!onEnded) return

View file

@ -3,6 +3,7 @@
import { subscribeKey } from 'valtio/utils'
import { isMobile } from 'renderer/viewer/lib/simpleUtils'
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
import { setSkinsConfig } from 'renderer/viewer/lib/utils/skins'
import { options, watchValue } from './optionsStorage'
import { reloadChunks } from './utils'
import { miscUiState } from './globalState'
@ -97,6 +98,8 @@ export const watchOptionsAfterViewerInit = () => {
appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor
appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading
appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks
setSkinsConfig({ apiEnabled: o.loadPlayerSkins })
})
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
@ -116,6 +119,10 @@ export const watchOptionsAfterViewerInit = () => {
appViewer.inWorldRenderingConfig.starfield = o.starfieldRendering
})
watchValue(options, o => {
appViewer.inWorldRenderingConfig.defaultSkybox = o.defaultSkybox
})
watchValue(options, o => {
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
})
@ -128,5 +135,6 @@ export const watchOptionsAfterWorldViewInit = (worldView: WorldDataEmitter) => {
appViewer.inWorldRenderingConfig.renderEars = o.renderEars
appViewer.inWorldRenderingConfig.showHand = o.showHand
appViewer.inWorldRenderingConfig.viewBobbing = o.viewBobbing
appViewer.inWorldRenderingConfig.dayCycle = o.dayCycleAndLighting
})
}