diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index f913b9b6..e80b7100 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -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: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cbf52251..3e8c4136 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
Minecraft Web Client to Minecraft Web Client — Free Online Browser Version
diff --git a/README.MD b/README.MD
index 7978cee5..018784e3 100644
--- a/README.MD
+++ b/README.MD
@@ -78,6 +78,8 @@ There is a builtin proxy, but you can also host your one! Just clone the repo, r
[](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=` - `` 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=` - 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
diff --git a/config.json b/config.json
index 940fb738..2bfa9cfe 100644
--- a/config.json
+++ b/config.json
@@ -10,6 +10,10 @@
{
"ip": "wss://play.mcraft.fun"
},
+ {
+ "ip": "wss://play.webmc.fun",
+ "name": "WebMC"
+ },
{
"ip": "wss://ws.fuchsmc.net"
},
diff --git a/package.json b/package.json
index a8c2c4e7..ff673726 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8acb9681..5bcd74a0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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
diff --git a/renderer/viewer/lib/createPlayerObject.ts b/renderer/viewer/lib/createPlayerObject.ts
new file mode 100644
index 00000000..836c8062
--- /dev/null
+++ b/renderer/viewer/lib/createPlayerObject.ts
@@ -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)
+}
diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts
index 86a85f77..dfbdb35c 100644
--- a/renderer/viewer/lib/worldDataEmitter.ts
+++ b/renderer/viewer/lib/worldDataEmitter.ts
@@ -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) {
@@ -360,8 +363,37 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter {
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
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
})
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
// (this).rerenderAllChunks?.()
// }
})
+
+ worldEmitter.on('biomeUpdate', ({ biome }) => {
+ this.biomeUpdated?.(biome)
+ })
+
+ worldEmitter.on('biomeReset', () => {
+ this.biomeReset?.()
+ })
}
setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {
diff --git a/renderer/viewer/three/cameraShake.ts b/renderer/viewer/three/cameraShake.ts
index 593b4628..7b159509 100644
--- a/renderer/viewer/three/cameraShake.ts
+++ b/renderer/viewer/three/cameraShake.ts
@@ -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
+ }
}
diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts
index 7849686b..fad30182 100644
--- a/renderer/viewer/three/entities.ts
+++ b/renderer/viewer/three/entities.ts
@@ -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
diff --git a/renderer/viewer/three/skyboxRenderer.ts b/renderer/viewer/three/skyboxRenderer.ts
index 294c72aa..fb9edae6 100644
--- a/renderer/viewer/three/skyboxRenderer.ts
+++ b/renderer/viewer/three/skyboxRenderer.ts
@@ -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 | 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)
+ }
}
}
diff --git a/renderer/viewer/three/waypointSprite.ts b/renderer/viewer/three/waypointSprite.ts
index 7c8cf1f6..6a30e6db 100644
--- a/renderer/viewer/three/waypointSprite.ts
+++ b/renderer/viewer/three/waypointSprite.ts
@@ -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
diff --git a/renderer/viewer/three/waypoints.ts b/renderer/viewer/three/waypoints.ts
index cebd779a..256ca6df 100644
--- a/renderer/viewer/three/waypoints.ts
+++ b/renderer/viewer/three/waypoints.ts
@@ -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)
diff --git a/renderer/viewer/three/world/cursorBlock.ts b/renderer/viewer/three/world/cursorBlock.ts
index b71c1b8d..a03a6999 100644
--- a/renderer/viewer/three/world/cursorBlock.ts
+++ b/renderer/viewer/three/world/cursorBlock.ts
@@ -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 () {
diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts
index fb6c8e11..1b4e6152 100644
--- a/renderer/viewer/three/worldrendererThree.ts
+++ b/renderer/viewer/three/worldrendererThree.ts
@@ -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, 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)
diff --git a/scripts/makeOptimizedMcData.mjs b/scripts/makeOptimizedMcData.mjs
index 76e0f1c2..a572d067 100644
--- a/scripts/makeOptimizedMcData.mjs
+++ b/scripts/makeOptimizedMcData.mjs
@@ -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,
diff --git a/src/appConfig.ts b/src/appConfig.ts
index 92fde21a..c29d74e8 100644
--- a/src/appConfig.ts
+++ b/src/appConfig.ts
@@ -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 // query string params
diff --git a/src/appParams.ts b/src/appParams.ts
index 8d487f8d..4c8ca186 100644
--- a/src/appParams.ts
+++ b/src/appParams.ts
@@ -47,6 +47,7 @@ export type AppQsParams = {
connectText?: string
freezeSettings?: string
testIosCrash?: string
+ addPing?: string
// Replay params
replayFilter?: string
diff --git a/src/basicSounds.ts b/src/basicSounds.ts
index 37f8dccd..54af0d35 100644
--- a/src/basicSounds.ts
+++ b/src/basicSounds.ts
@@ -7,7 +7,12 @@ let audioContext: AudioContext
const sounds: Record = {}
// 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)
})
diff --git a/src/chatUtils.ts b/src/chatUtils.ts
index 88437bc3..849d5847 100644
--- a/src/chatUtils.ts
+++ b/src/chatUtils.ts
@@ -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',
diff --git a/src/customChannels.ts b/src/customChannels.ts
index 717c7c93..506ea776 100644
--- a/src/customChannels.ts
+++ b/src/customChannels.ts
@@ -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
})
})
diff --git a/src/dayCycle.ts b/src/dayCycle.ts
deleted file mode 100644
index 50e63a21..00000000
--- a/src/dayCycle.ts
+++ /dev/null
@@ -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()
-}
diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts
index 6045e70b..48c1cfad 100644
--- a/src/defaultOptions.ts
+++ b/src/defaultOptions.ts
@@ -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,
diff --git a/src/devtools.ts b/src/devtools.ts
index 6c47f73d..1f8ef8e8 100644
--- a/src/devtools.ts
+++ b/src/devtools.ts
@@ -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)
diff --git a/src/entities.ts b/src/entities.ts
index dcec6143..674f91ef 100644
--- a/src/entities.ts
+++ b/src/entities.ts
@@ -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)) {
diff --git a/src/index.ts b/src/index.ts
index 4a118cee..7764188f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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 = () => {
diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts
index a9f89d1b..d40260df 100644
--- a/src/inventoryWindows.ts
+++ b/src/inventoryWindows.ts
@@ -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()
const cleanLoadedImagesCache = () => {
@@ -40,6 +42,34 @@ export const jeiCustomCategories = proxy({
value: [] as Array<{ id: string, categoryTitle: string, items: any[] }>
})
+let remotePlayerSkin: string | undefined | Promise
+
+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])
}
}
diff --git a/src/mineflayer/mc-protocol.ts b/src/mineflayer/mc-protocol.ts
index 0171387a..cd21d01f 100644
--- a/src/mineflayer/mc-protocol.ts
+++ b/src/mineflayer/mc-protocol.ts
@@ -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
diff --git a/src/mineflayer/plugins/mouse.ts b/src/mineflayer/plugins/mouse.ts
index fc1ce0fd..14e19345 100644
--- a/src/mineflayer/plugins/mouse.ts
+++ b/src/mineflayer/plugins/mouse.ts
@@ -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)
}
diff --git a/src/mineflayer/websocket-core.ts b/src/mineflayer/websocket-core.ts
index 0edd2497..f8163102 100644
--- a/src/mineflayer/websocket-core.ts
+++ b/src/mineflayer/websocket-core.ts
@@ -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)
})
diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx
index b03db37d..0cb0fe1e 100644
--- a/src/optionsGuiScheme.tsx
+++ b/src/optionsGuiScheme.tsx
@@ -480,6 +480,24 @@ export const guiOptionsScheme: {
],
sound: [
{ volume: {} },
+ {
+ custom () {
+ return {
+ options.musicVolume = value
+ }}
+ item={{
+ type: 'slider',
+ id: 'musicVolume',
+ text: 'Music Volume',
+ min: 0,
+ max: 100,
+ unit: '%',
+ }}
+ />
+ },
+ },
{
custom () {
return