Compare commits

..

35 commits

Author SHA1 Message Date
Vitaly
6a8d15b638 [deploy] properly destroy world view 2025-07-14 06:40:00 +03:00
Vitaly
b8c8f8ab62 Merge branch 'next' into light-engine 2025-07-14 06:36:05 +03:00
Vitaly
56aee16737
Merge branch 'next' into light-engine 2025-05-19 05:34:13 +03:00
Vitaly Turovsky
6be3c5c687 Merge remote-tracking branch 'origin/next' into light-engine 2025-05-08 20:28:33 +03:00
Vitaly Turovsky
f185df993f fix remaining issues with worker bundle with smart approach
todo: fix chunks not receive skylight, recompute
fix skylight values desync time
2025-05-08 20:28:00 +03:00
Vitaly Turovsky
90de0d0be1 up chunk? 2025-05-04 12:27:30 +03:00
Vitaly Turovsky
e95f84e92c fix lock 2025-05-04 12:26:15 +03:00
Vitaly Turovsky
7dba526ad8 Merge remote-tracking branch 'origin/next' into light-engine 2025-05-04 12:25:48 +03:00
Vitaly Turovsky
5720cfaf34 up light 2025-05-04 12:25:47 +03:00
Vitaly Turovsky
ddf08107f2 final step: move engine to another thread 2025-05-04 12:24:45 +03:00
Vitaly Turovsky
d6f394fe20 hide cursor block in spectator 2025-05-02 12:20:25 +03:00
Vitaly Turovsky
7d224fb7ef Merge remote-tracking branch 'origin/next' into light-engine 2025-05-01 15:28:09 +03:00
Vitaly
c4b9c33a3b
Update src/optionsStorage.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-05-01 15:26:22 +03:00
Vitaly Turovsky
c97c7e0cc0 finish combined computation, finish settings and strategies
todo: worker, horizontal sky fix, FIX SKY LEVEL from time!, fix block light blocking. OTHER done
2025-05-01 15:11:28 +03:00
Vitaly Turovsky
f4f5eddce0 fix lighting disabling
todo do sky light update re-rendering
when server: engine skip calc but stores 15 sky levels, it does recalc in worker and merge updated values
2025-04-30 09:37:25 +03:00
Vitaly Turovsky
27c55b1afc finish! 2025-04-30 09:14:37 +03:00
Vitaly Turovsky
79f0fdd86e fix lava rendering 2025-04-30 08:51:51 +03:00
Vitaly Turovsky
f4eab39f7f finish lighting 2025-04-30 08:51:46 +03:00
Vitaly Turovsky
2f6191a425 Merge remote-tracking branch 'origin/next' into light-engine 2025-04-30 08:14:38 +03:00
Vitaly
5a57d29919
Merge branch 'next' into light-engine 2025-04-28 09:47:39 +03:00
Vitaly Turovsky
1f5b682bee FINISH OPTIONS, FINISH RECOMPUTE, ADD LIGHT TO WATER 2025-04-28 09:46:59 +03:00
Vitaly Turovsky
b4c72dbb36 fix crash opt 2025-04-25 05:24:06 +03:00
Vitaly Turovsky
1918c68efb finish lighting 2025-04-25 04:49:31 +03:00
Vitaly Turovsky
3cd1ac3666 Merge branch 'next' into light-engine 2025-04-24 05:49:45 +03:00
Vitaly Turovsky
b1ba2cd470 Merge remote-tracking branch 'origin/next' into light-engine 2025-04-12 04:06:53 +03:00
Vitaly Turovsky
0fa66e295e Merge remote-tracking branch 'origin/next' into light-engine 2025-04-10 05:26:09 +03:00
Vitaly Turovsky
e10f610898 humble and terrible progress 2025-04-10 05:24:53 +03:00
Vitaly Turovsky
f18b3a17b3 Merge branch 'next' into light-engine 2025-04-07 20:17:13 +03:00
Vitaly Turovsky
9f505f81d6 rm workaround 2025-03-21 16:36:29 +03:00
Vitaly Turovsky
ec6b2494c8 Merge remote-tracking branch 'origin/next' into light-engine 2025-03-21 13:54:44 +03:00
Vitaly Turovsky
ace45a9f87 not crash pls 2025-03-14 00:03:33 +03:00
Vitaly Turovsky
037e297473 Merge remote-tracking branch 'origin/next' into light-engine 2025-03-12 18:23:55 +03:00
Vitaly Turovsky
48ead547e3 should work. 2025-03-12 18:23:37 +03:00
Vitaly Turovsky
d5c61d8320 a working light 2025-02-20 00:30:36 +03:00
Vitaly Turovsky
245300ff84 init 2025-02-18 21:48:18 +03:00
96 changed files with 1135 additions and 4519 deletions

View file

@ -23,7 +23,6 @@
// ], // ],
"@stylistic/arrow-spacing": "error", "@stylistic/arrow-spacing": "error",
"@stylistic/block-spacing": "error", "@stylistic/block-spacing": "error",
"@typescript-eslint/no-this-alias": "off",
"@stylistic/brace-style": [ "@stylistic/brace-style": [
"error", "error",
"1tbs", "1tbs",

View file

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

View file

@ -49,20 +49,6 @@ jobs:
publish_dir: .vercel/output/static publish_dir: .vercel/output/static
force_orphan: true 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 - name: Change index.html title
run: | run: |
# change <title>Minecraft Web Client</title> to <title>Minecraft Web Client — Free Online Browser Version</title> # change <title>Minecraft Web Client</title> to <title>Minecraft Web Client — Free Online Browser Version</title>

View file

@ -14,11 +14,13 @@ For building the project yourself / contributing, see [Development, Debugging &
> **Note**: You can deploy it on your own server in less than a minute using a one-liner script from [Minecraft Everywhere repo](https://github.com/zardoy/minecraft-everywhere) > **Note**: You can deploy it on your own server in less than a minute using a one-liner script from [Minecraft Everywhere repo](https://github.com/zardoy/minecraft-everywhere)
### Big Features ### Big Features
- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
- Combined Lighting System - Server Parsing + Client Side Engine for block updates
- Official Mineflayer [plugin integration](https://github.com/zardoy/mcraft-fun-mineflayer-plugin)! View / Control your bot remotely. - Official Mineflayer [plugin integration](https://github.com/zardoy/mcraft-fun-mineflayer-plugin)! View / Control your bot remotely.
- Open any zip world file or even folder in read-write mode! - Open any zip world file or even folder in read-write mode!
- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
- Integrated JS server clone capable of opening Java world saves in any way (folders, zip, web chunks streaming, etc) - Integrated JS server clone capable of opening Java world saves in any way (folders, zip, web chunks streaming, etc)
- Singleplayer mode with simple world generations! - Singleplayer mode with simple world generations!
- Works offline - Works offline
@ -54,9 +56,8 @@ Howerver, it's known that these browsers have issues:
### Versions Support ### Versions Support
Server versions 1.8 - 1.21.5 are supported. Server versions 1.8 - 1.21.4 are supported.
First class versions (most of the features are tested on these versions): First class versions (most of the features are tested on these versions):
- 1.19.4 - 1.19.4
- 1.21.4 - 1.21.4
@ -78,8 +79,6 @@ 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) [![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. 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 ```mermaid
@ -127,11 +126,11 @@ There is world renderer playground ([link](https://mcon.vercel.app/playground/))
However, there are many things that can be done in online production version (like debugging actual source code). Also you can access some global variables in the console and there are a few useful examples: However, there are many things that can be done in online production version (like debugging actual source code). Also you can access some global variables in the console and there are a few useful examples:
- If you type `debugToggle`, press enter in console - It will enables all debug messages! Warning: this will start all packets spam. - `localStorage.debug = '*'` - Enables all debug messages! Warning: this will start all packets spam.
Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name
- `bot` - Mineflayer bot instance. See Mineflayer documentation for more. - `bot` - Mineflayer bot instance. See Mineflayer documentation for more.
- `world` - Three.js world instance, basically does all the rendering (part of renderer backend). - `viewer` - Three.js viewer instance, basically does all the rendering.
- `world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group. - `world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk). - `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk).
- `debugChangedOptions` - See what options are changed. Don't change options here. - `debugChangedOptions` - See what options are changed. Don't change options here.
@ -141,7 +140,7 @@ Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can u
- `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read. - `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read.
The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `world.getCameraPosition()` to see the camera position and so on. The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `camera.position` to see the camera position and so on.
<img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/> <img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/>
@ -178,7 +177,6 @@ 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. - `?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. - `?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. - `?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: Single player specific:
@ -235,4 +233,3 @@ Only during development:
- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true) - [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) - [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

@ -1,2 +0,0 @@
here you can place custom textures for bundled files (blocks/items) e.g. blocks/stone.png
get file names from here (blocks/items) https://zardoy.github.io/mc-assets/

View file

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

View file

@ -1,5 +1,4 @@
{ {
"alwaysReconnectButton": true, "alwaysReconnectButton": true,
"reportBugButtonWithReconnect": true, "reportBugButtonWithReconnect": true
"allowAutoConnect": true
} }

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Minecraft Item Viewer</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script type="module" src="./three-item.ts"></script>
</body>
</html>

View file

@ -1,108 +0,0 @@
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import itemsAtlas from 'mc-assets/dist/itemsAtlasLegacy.png'
import { createItemMeshFromCanvas, createItemMesh } from '../renderer/viewer/three/itemMesh'
// Create scene, camera and renderer
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// Setup camera and controls
camera.position.set(0, 0, 3)
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
// Background and lights
scene.background = new THREE.Color(0x333333)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7)
scene.add(ambientLight)
// Animation loop
function animate () {
requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
async function setupItemMesh () {
try {
const loader = new THREE.TextureLoader()
const atlasTexture = await loader.loadAsync(itemsAtlas)
// Pixel-art configuration
atlasTexture.magFilter = THREE.NearestFilter
atlasTexture.minFilter = THREE.NearestFilter
atlasTexture.generateMipmaps = false
atlasTexture.wrapS = atlasTexture.wrapT = THREE.ClampToEdgeWrapping
// Extract the tile at x=2, y=0 (16x16)
const tileSize = 16
const tileX = 2
const tileY = 0
const canvas = document.createElement('canvas')
canvas.width = tileSize
canvas.height = tileSize
const ctx = canvas.getContext('2d')!
ctx.imageSmoothingEnabled = false
ctx.drawImage(
atlasTexture.image,
tileX * tileSize,
tileY * tileSize,
tileSize,
tileSize,
0,
0,
tileSize,
tileSize
)
// Test both approaches - working manual extraction:
const meshOld = createItemMeshFromCanvas(canvas, { depth: 0.1 })
meshOld.position.x = -1
meshOld.rotation.x = -Math.PI / 12
meshOld.rotation.y = Math.PI / 12
scene.add(meshOld)
// And new unified function:
const atlasWidth = atlasTexture.image.width
const atlasHeight = atlasTexture.image.height
const u = (tileX * tileSize) / atlasWidth
const v = (tileY * tileSize) / atlasHeight
const sizeX = tileSize / atlasWidth
const sizeY = tileSize / atlasHeight
console.log('Debug texture coords:', {u, v, sizeX, sizeY, atlasWidth, atlasHeight})
const resultNew = createItemMesh(atlasTexture, {
u, v, sizeX, sizeY
}, {
faceCamera: false,
use3D: true,
depth: 0.1
})
resultNew.mesh.position.x = 1
resultNew.mesh.rotation.x = -Math.PI / 12
resultNew.mesh.rotation.y = Math.PI / 12
scene.add(resultNew.mesh)
animate()
} catch (err) {
console.error('Failed to create item mesh:', err)
}
}
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
// Start
setupItemMesh()

View file

@ -1,5 +0,0 @@
<script type="module" src="three-labels.ts"></script>
<style>
body { margin: 0; }
canvas { display: block; }
</style>

View file

@ -1,67 +0,0 @@
import * as THREE from 'three'
import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'
import { createWaypointSprite, WAYPOINT_CONFIG } from '../renderer/viewer/three/waypointSprite'
// Create scene, camera and renderer
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// Add FirstPersonControls
const controls = new FirstPersonControls(camera, renderer.domElement)
controls.lookSpeed = 0.1
controls.movementSpeed = 10
controls.lookVertical = true
controls.constrainVertical = true
controls.verticalMin = 0.1
controls.verticalMax = Math.PI - 0.1
// Position camera
camera.position.y = 1.6 // Typical eye height
camera.lookAt(0, 1.6, -1)
// Create a helper grid and axes
const grid = new THREE.GridHelper(20, 20)
scene.add(grid)
const axes = new THREE.AxesHelper(5)
scene.add(axes)
// Create waypoint sprite via utility
const waypoint = createWaypointSprite({
position: new THREE.Vector3(0, 0, -5),
color: 0xff0000,
label: 'Target',
})
scene.add(waypoint.group)
// Use built-in offscreen arrow from utils
waypoint.enableOffscreenArrow(true)
waypoint.setArrowParent(scene)
// Animation loop
function animate() {
requestAnimationFrame(animate)
const delta = Math.min(clock.getDelta(), 0.1)
controls.update(delta)
// Unified camera update (size, distance text, arrow, visibility)
const sizeVec = renderer.getSize(new THREE.Vector2())
waypoint.updateForCamera(camera.position, camera, sizeVec.width, sizeVec.height)
renderer.render(scene, camera)
}
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
// Add clock for controls
const clock = new THREE.Clock()
animate()

View file

@ -54,7 +54,6 @@
"dependencies": { "dependencies": {
"@dimaka/interface": "0.0.3-alpha.0", "@dimaka/interface": "0.0.3-alpha.0",
"@floating-ui/react": "^0.26.1", "@floating-ui/react": "^0.26.1",
"@monaco-editor/react": "^4.7.0",
"@nxg-org/mineflayer-auto-jump": "^0.7.18", "@nxg-org/mineflayer-auto-jump": "^0.7.18",
"@nxg-org/mineflayer-tracker": "1.3.0", "@nxg-org/mineflayer-tracker": "1.3.0",
"@react-oauth/google": "^0.12.1", "@react-oauth/google": "^0.12.1",
@ -80,14 +79,14 @@
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"express": "^4.18.2", "express": "^4.18.2",
"filesize": "^10.0.12", "filesize": "^10.0.12",
"flying-squid": "npm:@zardoy/flying-squid@^0.0.104", "flying-squid": "npm:@zardoy/flying-squid@^0.0.62",
"framer-motion": "^12.9.2", "framer-motion": "^12.9.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"google-drive-browserfs": "github:zardoy/browserfs#google-drive", "google-drive-browserfs": "github:zardoy/browserfs#google-drive",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mcraft-fun-mineflayer": "^0.1.23", "mcraft-fun-mineflayer": "^0.1.23",
"minecraft-data": "3.98.0", "minecraft-data": "3.92.0",
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master", "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader", "mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
"mojangson": "^2.0.4", "mojangson": "^2.0.4",
@ -156,8 +155,10 @@
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"mc-assets": "^0.2.62", "mc-assets": "^0.2.62",
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
"minecraft-lighting": "^0.0.10",
"mineflayer": "github:zardoy/mineflayer#gen-the-master", "mineflayer": "github:zardoy/mineflayer#gen-the-master",
"mineflayer-mouse": "^0.1.21", "mineflayer-mouse": "^0.1.11",
"mineflayer-pathfinder": "^2.4.4",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
@ -197,7 +198,6 @@
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
"@nxg-org/mineflayer-physics-util": "1.8.10", "@nxg-org/mineflayer-physics-util": "1.8.10",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"vec3": "0.1.10", "vec3": "0.1.10",
@ -205,7 +205,7 @@
"diamond-square": "github:zardoy/diamond-square", "diamond-square": "github:zardoy/diamond-square",
"prismarine-block": "github:zardoy/prismarine-block#next-era", "prismarine-block": "github:zardoy/prismarine-block#next-era",
"prismarine-world": "github:zardoy/prismarine-world#next-era", "prismarine-world": "github:zardoy/prismarine-world#next-era",
"minecraft-data": "3.98.0", "minecraft-data": "3.92.0",
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything", "prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
"prismarine-physics": "github:zardoy/prismarine-physics", "prismarine-physics": "github:zardoy/prismarine-physics",
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master", "minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",

View file

@ -1,8 +1,8 @@
diff --git a/src/client/chat.js b/src/client/chat.js diff --git a/src/client/chat.js b/src/client/chat.js
index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b43cb102a 100644 index 8d0869b150681574ad19292a026cce9f67a137ee..2efa2e6600f017b566155974cb9fb1856fa582f9 100644
--- a/src/client/chat.js --- a/src/client/chat.js
+++ b/src/client/chat.js +++ b/src/client/chat.js
@@ -116,7 +116,7 @@ module.exports = function (client, options) { @@ -109,7 +109,7 @@ module.exports = function (client, options) {
for (const player of packet.data) { for (const player of packet.data) {
if (player.chatSession) { if (player.chatSession) {
client._players[player.uuid] = { client._players[player.uuid] = {
@ -11,7 +11,7 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
publicKeyDER: player.chatSession.publicKey.keyBytes, publicKeyDER: player.chatSession.publicKey.keyBytes,
sessionUuid: player.chatSession.uuid sessionUuid: player.chatSession.uuid
} }
@@ -126,7 +126,7 @@ module.exports = function (client, options) { @@ -119,7 +119,7 @@ module.exports = function (client, options) {
if (player.crypto) { if (player.crypto) {
client._players[player.uuid] = { client._players[player.uuid] = {
@ -20,7 +20,7 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
publicKeyDER: player.crypto.publicKey, publicKeyDER: player.crypto.publicKey,
signature: player.crypto.signature, signature: player.crypto.signature,
displayName: player.displayName || player.name displayName: player.displayName || player.name
@@ -196,7 +196,7 @@ module.exports = function (client, options) { @@ -189,7 +189,7 @@ module.exports = function (client, options) {
if (mcData.supportFeature('useChatSessions')) { if (mcData.supportFeature('useChatSessions')) {
const tsDelta = BigInt(Date.now()) - packet.timestamp const tsDelta = BigInt(Date.now()) - packet.timestamp
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0 const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
@ -28,8 +28,8 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
+ const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired + const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
if (verified) client._signatureCache.push(packet.signature) if (verified) client._signatureCache.push(packet.signature)
client.emit('playerChat', { client.emit('playerChat', {
globalIndex: packet.globalIndex, plainMessage: packet.plainMessage,
@@ -362,7 +362,7 @@ module.exports = function (client, options) { @@ -354,7 +354,7 @@ module.exports = function (client, options) {
} }
} }
@ -38,16 +38,16 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
options.timestamp = options.timestamp || BigInt(Date.now()) options.timestamp = options.timestamp || BigInt(Date.now())
options.salt = options.salt || 1n options.salt = options.salt || 1n
@@ -407,7 +407,7 @@ module.exports = function (client, options) { @@ -396,7 +396,7 @@ module.exports = function (client, options) {
message, message,
timestamp: options.timestamp, timestamp: options.timestamp,
salt: options.salt, salt: options.salt,
- signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, - signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
+ signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined, + signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
offset: client._lastSeenMessages.pending, offset: client._lastSeenMessages.pending,
checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+
acknowledged acknowledged
@@ -422,7 +422,7 @@ module.exports = function (client, options) { })
@@ -410,7 +410,7 @@ module.exports = function (client, options) {
message, message,
timestamp: options.timestamp, timestamp: options.timestamp,
salt: options.salt, salt: options.salt,
@ -57,7 +57,7 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
previousMessages: client._lastSeenMessages.map((e) => ({ previousMessages: client._lastSeenMessages.map((e) => ({
messageSender: e.sender, messageSender: e.sender,
diff --git a/src/client/encrypt.js b/src/client/encrypt.js diff --git a/src/client/encrypt.js b/src/client/encrypt.js
index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f85410eaef 100644 index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108f4c63536 100644
--- a/src/client/encrypt.js --- a/src/client/encrypt.js
+++ b/src/client/encrypt.js +++ b/src/client/encrypt.js
@@ -25,7 +25,11 @@ module.exports = function (client, options) { @@ -25,7 +25,11 @@ module.exports = function (client, options) {
@ -73,24 +73,41 @@ index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f8
} }
function onJoinServerResponse (err) { function onJoinServerResponse (err) {
diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js diff --git a/src/client/play.js b/src/client/play.js
index 671eb452f31e6b5fcd57d715f1009d010160c65f..7f69f511c8fb97d431ec5125c851b49be8e2ab76 100644 index 559607f34e9a5b2b7809423f8ca4cd6746b60225..4dc1c3139438cc2729b05c57e57bd00252728f8a 100644
--- a/src/client/pluginChannels.js --- a/src/client/play.js
+++ b/src/client/pluginChannels.js +++ b/src/client/play.js
@@ -57,7 +57,7 @@ module.exports = function (client, options) { @@ -53,7 +53,7 @@ module.exports = function (client, options) {
try { client.write('configuration_acknowledged', {})
packet.data = proto.parsePacketBuffer(channel, packet.data).data
} catch (error) {
- client.emit('error', error)
+ client.emit('error', error, { customPayload: packet })
return
}
} }
client.state = states.CONFIGURATION
- client.on('select_known_packs', () => {
+ client.once('select_known_packs', () => {
client.write('select_known_packs', { packs: [] })
})
// Server should send finish_configuration on its own right after sending the client a dimension codec
diff --git a/src/client.js b/src/client.js diff --git a/src/client.js b/src/client.js
index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875989cdf0c 100644 index 5c7a62b013daa69be91ec9e763b1f48ffe96ffa6..174d42a77740a937afcb106e1f39a9ee824a24b9 100644
--- a/src/client.js --- a/src/client.js
+++ b/src/client.js +++ b/src/client.js
@@ -111,7 +111,13 @@ class Client extends EventEmitter { @@ -89,10 +89,12 @@ class Client extends EventEmitter {
parsed.metadata.name = parsed.data.name
parsed.data = parsed.data.params
parsed.metadata.state = state
- debug('read packet ' + state + '.' + parsed.metadata.name)
- if (debug.enabled) {
- const s = JSON.stringify(parsed.data, null, 2)
- debug(s && s.length > 10000 ? parsed.data : s)
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) {
+ debug('read packet ' + state + '.' + parsed.metadata.name)
+ if (debug.enabled) {
+ const s = JSON.stringify(parsed.data, null, 2)
+ debug(s && s.length > 10000 ? parsed.data : s)
+ }
}
if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') {
if (this._mcBundle.length) { // End bundle
@@ -110,7 +112,13 @@ class Client extends EventEmitter {
this._hasBundlePacket = false this._hasBundlePacket = false
} }
} else { } else {
@ -105,7 +122,7 @@ index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875
} }
}) })
} }
@@ -169,7 +175,10 @@ class Client extends EventEmitter { @@ -168,7 +176,10 @@ class Client extends EventEmitter {
} }
const onFatalError = (err) => { const onFatalError = (err) => {
@ -117,21 +134,25 @@ index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875
endSocket() endSocket()
} }
@@ -198,6 +207,10 @@ class Client extends EventEmitter { @@ -197,6 +208,8 @@ class Client extends EventEmitter {
serializer -> framer -> socket -> splitter -> deserializer */ serializer -> framer -> socket -> splitter -> deserializer */
if (this.serializer) { if (this.serializer) {
this.serializer.end() this.serializer.end()
+ setTimeout(() => { + this.socket?.end()
+ this.socket?.end() + this.socket?.emit('end')
+ this.socket?.emit('end')
+ }, 2000) // allow the serializer to finish writing
} else { } else {
if (this.socket) this.socket.end() if (this.socket) this.socket.end()
} }
@@ -243,6 +256,7 @@ class Client extends EventEmitter { @@ -238,8 +251,11 @@ class Client extends EventEmitter {
debug('writing packet ' + this.state + '.' + name)
debug(params) write (name, params) {
} if (!this.serializer.writable) { return }
- debug('writing packet ' + this.state + '.' + name)
- debug(params)
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) {
+ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name)
+ debug(params)
+ }
+ this.emit('writePacket', name, params) + this.emit('writePacket', name, params)
this.serializer.write({ name, params }) this.serializer.write({ name, params })
} }

288
pnpm-lock.yaml generated
View file

@ -5,7 +5,6 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
overrides: overrides:
mineflayer: github:zardoy/mineflayer#gen-the-master
'@nxg-org/mineflayer-physics-util': 1.8.10 '@nxg-org/mineflayer-physics-util': 1.8.10
buffer: ^6.0.3 buffer: ^6.0.3
vec3: 0.1.10 vec3: 0.1.10
@ -13,7 +12,7 @@ overrides:
diamond-square: github:zardoy/diamond-square diamond-square: github:zardoy/diamond-square
prismarine-block: github:zardoy/prismarine-block#next-era prismarine-block: github:zardoy/prismarine-block#next-era
prismarine-world: github:zardoy/prismarine-world#next-era prismarine-world: github:zardoy/prismarine-world#next-era
minecraft-data: 3.98.0 minecraft-data: 3.92.0
prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything
prismarine-physics: github:zardoy/prismarine-physics prismarine-physics: github:zardoy/prismarine-physics
minecraft-protocol: github:PrismarineJS/node-minecraft-protocol#master minecraft-protocol: github:PrismarineJS/node-minecraft-protocol#master
@ -23,7 +22,7 @@ overrides:
patchedDependencies: patchedDependencies:
minecraft-protocol: minecraft-protocol:
hash: 4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b hash: a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0
path: patches/minecraft-protocol.patch path: patches/minecraft-protocol.patch
mineflayer-item-map-downloader@1.2.0: mineflayer-item-map-downloader@1.2.0:
hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad
@ -42,9 +41,6 @@ importers:
'@floating-ui/react': '@floating-ui/react':
specifier: ^0.26.1 specifier: ^0.26.1
version: 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@monaco-editor/react':
specifier: ^4.7.0
version: 4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@nxg-org/mineflayer-auto-jump': '@nxg-org/mineflayer-auto-jump':
specifier: ^0.7.18 specifier: ^0.7.18
version: 0.7.18 version: 0.7.18
@ -121,8 +117,8 @@ importers:
specifier: ^10.0.12 specifier: ^10.0.12
version: 10.1.6 version: 10.1.6
flying-squid: flying-squid:
specifier: npm:@zardoy/flying-squid@^0.0.104 specifier: npm:@zardoy/flying-squid@^0.0.62
version: '@zardoy/flying-squid@0.0.104(encoding@0.1.13)' version: '@zardoy/flying-squid@0.0.62(encoding@0.1.13)'
framer-motion: framer-motion:
specifier: ^12.9.2 specifier: ^12.9.2
version: 12.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 12.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -140,13 +136,13 @@ importers:
version: 4.17.21 version: 4.17.21
mcraft-fun-mineflayer: mcraft-fun-mineflayer:
specifier: ^0.1.23 specifier: ^0.1.23
version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)) version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13))
minecraft-data: minecraft-data:
specifier: 3.98.0 specifier: 3.92.0
version: 3.98.0 version: 3.92.0
minecraft-protocol: minecraft-protocol:
specifier: github:PrismarineJS/node-minecraft-protocol#master specifier: github:PrismarineJS/node-minecraft-protocol#master
version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13)
mineflayer-item-map-downloader: mineflayer-item-map-downloader:
specifier: github:zardoy/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) version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13)
@ -155,7 +151,7 @@ importers:
version: 2.0.4 version: 2.0.4
net-browserify: net-browserify:
specifier: github:zardoy/prismarinejs-net-browserify specifier: github:zardoy/prismarinejs-net-browserify
version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618 version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5
node-gzip: node-gzip:
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2 version: 1.1.2
@ -170,7 +166,7 @@ importers:
version: 6.1.1 version: 6.1.1
prismarine-provider-anvil: prismarine-provider-anvil:
specifier: github:zardoy/prismarine-provider-anvil#everything specifier: github:zardoy/prismarine-provider-anvil#everything
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0) version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
prosemirror-example-setup: prosemirror-example-setup:
specifier: ^1.2.2 specifier: ^1.2.2
version: 1.2.3 version: 1.2.3
@ -343,12 +339,18 @@ importers:
minecraft-inventory-gui: minecraft-inventory-gui:
specifier: github:zardoy/minecraft-inventory-gui#next specifier: github:zardoy/minecraft-inventory-gui#next
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1) version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1)
minecraft-lighting:
specifier: ^0.0.10
version: 0.0.10
mineflayer: mineflayer:
specifier: github:zardoy/mineflayer#gen-the-master specifier: github:zardoy/mineflayer#gen-the-master
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13) version: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13)
mineflayer-mouse: mineflayer-mouse:
specifier: ^0.1.21 specifier: ^0.1.11
version: 0.1.21 version: 0.1.11
mineflayer-pathfinder:
specifier: ^2.4.4
version: 2.4.5
npm-run-all: npm-run-all:
specifier: ^4.1.5 specifier: ^4.1.5
version: 4.1.5 version: 4.1.5
@ -436,7 +438,7 @@ importers:
version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-chunk: prismarine-chunk:
specifier: github:zardoy/prismarine-chunk#master specifier: github:zardoy/prismarine-chunk#master
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-schematic: prismarine-schematic:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.3 version: 1.2.3
@ -1992,16 +1994,6 @@ packages:
'@module-federation/webpack-bundler-runtime@0.11.2': '@module-federation/webpack-bundler-runtime@0.11.2':
resolution: {integrity: sha512-WdwIE6QF+MKs/PdVu0cKPETF743JB9PZ62/qf7Uo3gU4fjsUMc37RnbJZ/qB60EaHHfjwp1v6NnhZw1r4eVsnw==} resolution: {integrity: sha512-WdwIE6QF+MKs/PdVu0cKPETF743JB9PZ62/qf7Uo3gU4fjsUMc37RnbJZ/qB60EaHHfjwp1v6NnhZw1r4eVsnw==}
'@monaco-editor/loader@1.5.0':
resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==}
'@monaco-editor/react@4.7.0':
resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==}
peerDependencies:
monaco-editor: '>= 0.25.0 < 1'
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@msgpack/msgpack@2.8.0': '@msgpack/msgpack@2.8.0':
resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@ -3387,13 +3379,13 @@ packages:
resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==} resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==}
engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'} engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'}
'@zardoy/flying-squid@0.0.104': '@zardoy/flying-squid@0.0.49':
resolution: {integrity: sha512-jGhQ7fn7o8UN+mUwZbt9674D37YLuBi+Au4TwKcopCA6huIQdHTFNl2e+0ZSTI5mnhN+NpyVoR3vmtH6L58vHQ==} resolution: {integrity: sha512-Kt4wr5/R+44tcLU9gjuNG2an9weWeKEpIoKXfsgJN2GGQqdnbd5nBpxfGDdgZ9aMdFugsVW8BsyPZNhj9vbMXA==}
engines: {node: '>=8'} engines: {node: '>=8'}
hasBin: true hasBin: true
'@zardoy/flying-squid@0.0.49': '@zardoy/flying-squid@0.0.62':
resolution: {integrity: sha512-Kt4wr5/R+44tcLU9gjuNG2an9weWeKEpIoKXfsgJN2GGQqdnbd5nBpxfGDdgZ9aMdFugsVW8BsyPZNhj9vbMXA==} resolution: {integrity: sha512-M6icydO/yrmwevBhmgKcqEPC63AhWfU/Es9N/uadVrmKaxGm2FQMMLcybbutRYm1xZ6qsdxDUOUZnN56PsVwfQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
hasBin: true hasBin: true
@ -6444,12 +6436,6 @@ packages:
resolution: {integrity: sha512-RYZeD1+joNlPuUpi+tIWkbP0ieVJr+R6IFkI6/8juhSxx9zE4osoSmteybrfspGm8A6u+YbbY1epqRKEMwVR6Q==} resolution: {integrity: sha512-RYZeD1+joNlPuUpi+tIWkbP0ieVJr+R6IFkI6/8juhSxx9zE4osoSmteybrfspGm8A6u+YbbY1epqRKEMwVR6Q==}
engines: {node: '>=18.0.0'} 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: mcraft-fun-mineflayer@0.1.23:
resolution: {integrity: sha512-qmI1rQQ0Ro5zJdi99rClWLF+mS9JZffgNX2vyWWesS3Hsk3Xblp/8swYTJKHSaFpNgzkVfXV92fEIrBqeH6iKA==} resolution: {integrity: sha512-qmI1rQQ0Ro5zJdi99rClWLF+mS9JZffgNX2vyWWesS3Hsk3Xblp/8swYTJKHSaFpNgzkVfXV92fEIrBqeH6iKA==}
version: 0.1.23 version: 0.1.23
@ -6658,8 +6644,8 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'} engines: {node: '>=4'}
minecraft-data@3.98.0: minecraft-data@3.92.0:
resolution: {integrity: sha512-JAPqJ/TZoxMUlAPPdWUh1v5wdqvYGFSZ4rW9bUtmaKBkGpomDSjw4V02ocBqbxKJvcTtmc5nM/LfN9/0DDqHrQ==} resolution: {integrity: sha512-CGfO50svzm+pSRa4Mbq4owsmRKbPCNkSZ3MCOyH+epC7yNjh+PUhPQFHWq72O51qsY7pAB5qM/bJn1ncwG1J5g==}
minecraft-folder-path@1.2.0: minecraft-folder-path@1.2.0:
resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==} resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==}
@ -6668,9 +6654,13 @@ packages:
resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41} resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41}
version: 1.0.1 version: 1.0.1
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9: minecraft-lighting@0.0.10:
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9} resolution: {integrity: sha512-m3RNe5opaibquxyO0ly1FpKdehapvp9hRRY37RccKY4bio2LGnN3nCZ3PrOXy0C596YpxBsG1OCYg0dqtPzehg==}
version: 1.62.0 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176:
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176}
version: 1.58.0
engines: {node: '>=22'} engines: {node: '>=22'}
minecraft-wrap@1.6.0: minecraft-wrap@1.6.0:
@ -6684,13 +6674,20 @@ packages:
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824} resolution: {tarball: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824}
version: 1.2.0 version: 1.2.0
mineflayer-mouse@0.1.21: mineflayer-mouse@0.1.11:
resolution: {integrity: sha512-1XTVuw3twIrEcqQ1QRSB8NcStIUEZ+tbxiAG6rOrN/9M4thhtlS5PTJzFdmdrcYgWEBLvuOdJszaKE5zFfiXhg==} resolution: {integrity: sha512-BL47pXZ1+92BA/7ym6KaJctEHKnL0up+tpuagVwSKJvAgibeqWQJJwDlNUWkOLvpnruRKDxMR5OB1hUXFoDNSg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659: mineflayer-pathfinder@2.4.5:
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659} resolution: {integrity: sha512-Jh3JnUgRLwhMh2Dugo4SPza68C41y+NPP5sdsgxRu35ydndo70i1JJGxauVWbXrpNwIxYNztUw78aFyb7icw8g==}
version: 8.0.0
mineflayer@4.30.0:
resolution: {integrity: sha512-GtW4hkijyZbSu5LKYYD89xZu+XY7OoP7IkrCnNEn6EdPm0+vr2THoJgFGKrlze9/81+T+P3E4qvJXNFiU/zeJg==}
engines: {node: '>=22'}
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980:
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980}
version: 4.30.0
engines: {node: '>=22'} engines: {node: '>=22'}
minimalistic-assert@1.0.1: minimalistic-assert@1.0.1:
@ -6788,9 +6785,6 @@ packages:
mojangson@2.0.4: mojangson@2.0.4:
resolution: {integrity: sha512-HYmhgDjr1gzF7trGgvcC/huIg2L8FsVbi/KacRe6r1AswbboGVZDS47SOZlomPuMWvZLas8m9vuHHucdZMwTmQ==} resolution: {integrity: sha512-HYmhgDjr1gzF7trGgvcC/huIg2L8FsVbi/KacRe6r1AswbboGVZDS47SOZlomPuMWvZLas8m9vuHHucdZMwTmQ==}
monaco-editor@0.52.2:
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
moo@0.5.2: moo@0.5.2:
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
@ -6855,8 +6849,8 @@ packages:
neo-async@2.6.2: neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618: 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/e754999ffdea67853bc9b10553b5e9908b40f618} resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5}
version: 0.2.4 version: 0.2.4
nice-try@1.0.5: nice-try@1.0.5:
@ -7387,7 +7381,7 @@ packages:
prismarine-biome@1.3.0: prismarine-biome@1.3.0:
resolution: {integrity: sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==} resolution: {integrity: sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==}
peerDependencies: peerDependencies:
minecraft-data: 3.98.0 minecraft-data: 3.92.0
prismarine-registry: ^1.1.0 prismarine-registry: ^1.1.0
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9:
@ -7405,8 +7399,8 @@ packages:
prismarine-entity@2.5.0: prismarine-entity@2.5.0:
resolution: {integrity: sha512-nRPCawUwf9r3iKqi4I7mZRlir1Ix+DffWYdWq6p/KNnmiXve+xHE5zv8XCdhZlUmOshugHv5ONl9o6ORAkCNIA==} resolution: {integrity: sha512-nRPCawUwf9r3iKqi4I7mZRlir1Ix+DffWYdWq6p/KNnmiXve+xHE5zv8XCdhZlUmOshugHv5ONl9o6ORAkCNIA==}
prismarine-item@1.17.0: prismarine-item@1.16.0:
resolution: {integrity: sha512-wN1OjP+f+Uvtjo3KzeCkVSy96CqZ8yG7cvuvlGwcYupQ6ct7LtNkubHp0AHuLMJ0vbbfAC0oZ2bWOgI1DYp8WA==} resolution: {integrity: sha512-88Tz+/6HquYIsDuseae5G3IbqLeMews2L+ba2gX+p6K6soU9nuFhCfbwN56QuB7d/jZFcWrCYAPE5+UhwWh67w==}
prismarine-nbt@2.7.0: prismarine-nbt@2.7.0:
resolution: {integrity: sha512-Du9OLQAcCj3y29YtewOJbbV4ARaSUEJiTguw0PPQbPBy83f+eCyDRkyBpnXTi/KPyEpgYCzsjGzElevLpFoYGQ==} resolution: {integrity: sha512-Du9OLQAcCj3y29YtewOJbbV4ARaSUEJiTguw0PPQbPBy83f+eCyDRkyBpnXTi/KPyEpgYCzsjGzElevLpFoYGQ==}
@ -8337,7 +8331,6 @@ packages:
source-map@0.8.0-beta.0: source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
sourcemap-codec@1.4.8: sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
@ -8395,9 +8388,6 @@ packages:
stacktrace-js@2.0.2: stacktrace-js@2.0.2:
resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==}
state-local@1.0.7:
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
static-extend@0.1.2: static-extend@0.1.2:
resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -9685,7 +9675,7 @@ snapshots:
'@babel/core': 7.26.9 '@babel/core': 7.26.9
'@babel/helper-compilation-targets': 7.26.5 '@babel/helper-compilation-targets': 7.26.5
'@babel/helper-plugin-utils': 7.26.5 '@babel/helper-plugin-utils': 7.26.5
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
lodash.debounce: 4.0.8 lodash.debounce: 4.0.8
resolve: 1.22.10 resolve: 1.22.10
transitivePeerDependencies: transitivePeerDependencies:
@ -10310,7 +10300,7 @@ snapshots:
'@babel/parser': 7.26.9 '@babel/parser': 7.26.9
'@babel/template': 7.26.9 '@babel/template': 7.26.9
'@babel/types': 7.26.9 '@babel/types': 7.26.9
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
globals: 11.12.0 globals: 11.12.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -11279,17 +11269,6 @@ snapshots:
'@module-federation/runtime': 0.11.2 '@module-federation/runtime': 0.11.2
'@module-federation/sdk': 0.11.2 '@module-federation/sdk': 0.11.2
'@monaco-editor/loader@1.5.0':
dependencies:
state-local: 1.0.7
'@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@monaco-editor/loader': 1.5.0
monaco-editor: 0.52.2
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@msgpack/msgpack@2.8.0': {} '@msgpack/msgpack@2.8.0': {}
'@ndelangen/get-tarball@3.0.9': '@ndelangen/get-tarball@3.0.9':
@ -11343,10 +11322,10 @@ snapshots:
'@nxg-org/mineflayer-trajectories@1.2.0(encoding@0.1.13)': '@nxg-org/mineflayer-trajectories@1.2.0(encoding@0.1.13)':
dependencies: dependencies:
'@nxg-org/mineflayer-util-plugin': 1.8.4 '@nxg-org/mineflayer-util-plugin': 1.8.4
minecraft-data: 3.98.0 minecraft-data: 3.92.0
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13) mineflayer: 4.30.0(encoding@0.1.13)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-item: 1.17.0 prismarine-item: 1.16.0
prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b
vec3: 0.1.10 vec3: 0.1.10
transitivePeerDependencies: transitivePeerDependencies:
@ -12891,7 +12870,7 @@ snapshots:
dependencies: dependencies:
'@typescript-eslint/typescript-estree': 6.1.0(typescript@5.5.4) '@typescript-eslint/typescript-estree': 6.1.0(typescript@5.5.4)
'@typescript-eslint/utils': 6.1.0(eslint@8.57.1)(typescript@5.5.4) '@typescript-eslint/utils': 6.1.0(eslint@8.57.1)(typescript@5.5.4)
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
eslint: 8.57.1 eslint: 8.57.1
ts-api-utils: 1.4.3(typescript@5.5.4) ts-api-utils: 1.4.3(typescript@5.5.4)
optionalDependencies: optionalDependencies:
@ -12909,7 +12888,7 @@ snapshots:
dependencies: dependencies:
'@typescript-eslint/types': 6.1.0 '@typescript-eslint/types': 6.1.0
'@typescript-eslint/visitor-keys': 6.1.0 '@typescript-eslint/visitor-keys': 6.1.0
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
globby: 11.1.0 globby: 11.1.0
is-glob: 4.0.3 is-glob: 4.0.3
semver: 7.7.1 semver: 7.7.1
@ -12923,7 +12902,7 @@ snapshots:
dependencies: dependencies:
'@typescript-eslint/types': 6.21.0 '@typescript-eslint/types': 6.21.0
'@typescript-eslint/visitor-keys': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
globby: 11.1.0 globby: 11.1.0
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.3 minimatch: 9.0.3
@ -12938,7 +12917,7 @@ snapshots:
dependencies: dependencies:
'@typescript-eslint/types': 8.26.0 '@typescript-eslint/types': 8.26.0
'@typescript-eslint/visitor-keys': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
fast-glob: 3.3.3 fast-glob: 3.3.3
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.5 minimatch: 9.0.5
@ -13094,7 +13073,7 @@ snapshots:
'@types/emscripten': 1.40.0 '@types/emscripten': 1.40.0
tslib: 1.14.1 tslib: 1.14.1
'@zardoy/flying-squid@0.0.104(encoding@0.1.13)': '@zardoy/flying-squid@0.0.49(encoding@0.1.13)':
dependencies: dependencies:
'@tootallnate/once': 2.0.0 '@tootallnate/once': 2.0.0
chalk: 5.4.1 chalk: 5.4.1
@ -13104,18 +13083,16 @@ snapshots:
exit-hook: 2.2.1 exit-hook: 2.2.1
flatmap: 0.0.3 flatmap: 0.0.3
long: 5.3.1 long: 5.3.1
mc-bridge: 0.1.3(minecraft-data@3.98.0) minecraft-data: 3.92.0
minecraft-data: 3.98.0 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13)
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
mkdirp: 2.1.6 mkdirp: 2.1.6
node-gzip: 1.1.2 node-gzip: 1.1.2
node-rsa: 1.1.1 node-rsa: 1.1.1
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-entity: 2.5.0 prismarine-entity: 2.5.0
prismarine-item: 1.17.0 prismarine-item: 1.16.0
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0) prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
prismarine-windows: 2.9.0 prismarine-windows: 2.9.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
rambda: 9.4.2 rambda: 9.4.2
@ -13132,7 +13109,7 @@ snapshots:
- encoding - encoding
- supports-color - supports-color
'@zardoy/flying-squid@0.0.49(encoding@0.1.13)': '@zardoy/flying-squid@0.0.62(encoding@0.1.13)':
dependencies: dependencies:
'@tootallnate/once': 2.0.0 '@tootallnate/once': 2.0.0
chalk: 5.4.1 chalk: 5.4.1
@ -13142,16 +13119,16 @@ snapshots:
exit-hook: 2.2.1 exit-hook: 2.2.1
flatmap: 0.0.3 flatmap: 0.0.3
long: 5.3.1 long: 5.3.1
minecraft-data: 3.98.0 minecraft-data: 3.92.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13)
mkdirp: 2.1.6 mkdirp: 2.1.6
node-gzip: 1.1.2 node-gzip: 1.1.2
node-rsa: 1.1.1 node-rsa: 1.1.1
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-entity: 2.5.0 prismarine-entity: 2.5.0
prismarine-item: 1.17.0 prismarine-item: 1.16.0
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0) prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
prismarine-windows: 2.9.0 prismarine-windows: 2.9.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
rambda: 9.4.2 rambda: 9.4.2
@ -14532,7 +14509,7 @@ snapshots:
detect-port@1.6.1: detect-port@1.6.1:
dependencies: dependencies:
address: 1.2.2 address: 1.2.2
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -14542,8 +14519,8 @@ snapshots:
diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71: diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71:
dependencies: dependencies:
minecraft-data: 3.98.0 minecraft-data: 3.92.0
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
random-seed: 0.3.0 random-seed: 0.3.0
vec3: 0.1.10 vec3: 0.1.10
@ -16139,7 +16116,7 @@ snapshots:
https-proxy-agent@4.0.0: https-proxy-agent@4.0.0:
dependencies: dependencies:
agent-base: 5.1.1 agent-base: 5.1.1
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -16986,17 +16963,13 @@ snapshots:
maxrects-packer: '@zardoy/maxrects-packer@2.7.4' maxrects-packer: '@zardoy/maxrects-packer@2.7.4'
zod: 3.24.2 zod: 3.24.2
mc-bridge@0.1.3(minecraft-data@3.98.0): mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13)):
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: dependencies:
'@zardoy/flying-squid': 0.0.49(encoding@0.1.13) '@zardoy/flying-squid': 0.0.49(encoding@0.1.13)
exit-hook: 2.2.1 exit-hook: 2.2.1
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13)
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13) mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13)
prismarine-item: 1.17.0 prismarine-item: 1.16.0
ws: 8.18.1 ws: 8.18.1
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
@ -17225,7 +17198,7 @@ snapshots:
micromark@4.0.2: micromark@4.0.2:
dependencies: dependencies:
'@types/debug': 4.1.12 '@types/debug': 4.1.12
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
decode-named-character-reference: 1.1.0 decode-named-character-reference: 1.1.0
devlop: 1.1.0 devlop: 1.1.0
micromark-core-commonmark: 2.0.3 micromark-core-commonmark: 2.0.3
@ -17302,7 +17275,7 @@ snapshots:
min-indent@1.0.1: {} min-indent@1.0.1: {}
minecraft-data@3.98.0: {} minecraft-data@3.92.0: {}
minecraft-folder-path@1.2.0: {} minecraft-folder-path@1.2.0: {}
@ -17313,7 +17286,11 @@ snapshots:
- '@types/react' - '@types/react'
- react - react
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13): minecraft-lighting@0.0.10:
dependencies:
vec3: 0.1.10
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13):
dependencies: dependencies:
'@types/node-rsa': 1.1.4 '@types/node-rsa': 1.1.4
'@types/readable-stream': 4.0.18 '@types/readable-stream': 4.0.18
@ -17322,7 +17299,7 @@ snapshots:
debug: 4.4.0(supports-color@8.1.1) debug: 4.4.0(supports-color@8.1.1)
endian-toggle: 0.0.0 endian-toggle: 0.0.0
lodash.merge: 4.6.2 lodash.merge: 4.6.2
minecraft-data: 3.98.0 minecraft-data: 3.92.0
minecraft-folder-path: 1.2.0 minecraft-folder-path: 1.2.0
node-fetch: 2.7.0(encoding@0.1.13) node-fetch: 2.7.0(encoding@0.1.13)
node-rsa: 0.4.2 node-rsa: 0.4.2
@ -17365,32 +17342,65 @@ snapshots:
mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13): mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13):
dependencies: dependencies:
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13) mineflayer: 4.30.0(encoding@0.1.13)
sharp: 0.30.7 sharp: 0.30.7
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- supports-color - supports-color
mineflayer-mouse@0.1.21: mineflayer-mouse@0.1.11:
dependencies: dependencies:
change-case: 5.4.4 change-case: 5.4.4
debug: 4.4.1 debug: 4.4.1
prismarine-item: 1.17.0 prismarine-item: 1.16.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13): mineflayer-pathfinder@2.4.5:
dependencies: dependencies:
'@nxg-org/mineflayer-physics-util': 1.8.10 minecraft-data: 3.92.0
minecraft-data: 3.98.0 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13) prismarine-entity: 2.5.0
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0) prismarine-item: 1.16.0
prismarine-nbt: 2.7.0
prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b
vec3: 0.1.10
mineflayer@4.30.0(encoding@0.1.13):
dependencies:
minecraft-data: 3.92.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13)
prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-chat: 1.11.0 prismarine-chat: 1.11.0
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0) prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-entity: 2.5.0 prismarine-entity: 2.5.0
prismarine-item: 1.17.0 prismarine-item: 1.16.0
prismarine-nbt: 2.7.0
prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b
prismarine-recipe: 1.3.1(prismarine-registry@1.11.0)
prismarine-registry: 1.11.0
prismarine-windows: 2.9.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
protodef: 1.18.0
typed-emitter: 1.4.0
vec3: 0.1.10
transitivePeerDependencies:
- encoding
- supports-color
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(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/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13)
prismarine-biome: 1.3.0(minecraft-data@3.92.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-entity: 2.5.0
prismarine-item: 1.16.0
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b
prismarine-recipe: 1.3.1(prismarine-registry@1.11.0) prismarine-recipe: 1.3.1(prismarine-registry@1.11.0)
@ -17503,8 +17513,6 @@ snapshots:
dependencies: dependencies:
nearley: 2.20.1 nearley: 2.20.1
monaco-editor@0.52.2: {}
moo@0.5.2: {} moo@0.5.2: {}
morgan@1.10.0: morgan@1.10.0:
@ -17586,7 +17594,7 @@ snapshots:
neo-async@2.6.2: {} neo-async@2.6.2: {}
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618: net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5:
dependencies: dependencies:
body-parser: 1.20.3 body-parser: 1.20.3
express: 4.21.2 express: 4.21.2
@ -18174,17 +18182,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
prismarine-biome@1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0): prismarine-biome@1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0):
dependencies: dependencies:
minecraft-data: 3.98.0 minecraft-data: 3.92.0
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9:
dependencies: dependencies:
minecraft-data: 3.98.0 minecraft-data: 3.92.0
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
prismarine-chat: 1.11.0 prismarine-chat: 1.11.0
prismarine-item: 1.17.0 prismarine-item: 1.16.0
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
@ -18194,9 +18202,9 @@ snapshots:
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0): prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0):
dependencies: dependencies:
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0) prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
@ -18210,11 +18218,11 @@ snapshots:
prismarine-entity@2.5.0: prismarine-entity@2.5.0:
dependencies: dependencies:
prismarine-chat: 1.11.0 prismarine-chat: 1.11.0
prismarine-item: 1.17.0 prismarine-item: 1.16.0
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
vec3: 0.1.10 vec3: 0.1.10
prismarine-item@1.17.0: prismarine-item@1.16.0:
dependencies: dependencies:
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
@ -18225,14 +18233,14 @@ snapshots:
prismarine-physics@https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b: prismarine-physics@https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b:
dependencies: dependencies:
minecraft-data: 3.98.0 minecraft-data: 3.92.0
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
vec3: 0.1.10 vec3: 0.1.10
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0): prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0):
dependencies: dependencies:
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 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-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
uint4: 0.1.2 uint4: 0.1.2
@ -18254,13 +18262,13 @@ snapshots:
prismarine-registry@1.11.0: prismarine-registry@1.11.0:
dependencies: dependencies:
minecraft-data: 3.98.0 minecraft-data: 3.92.0
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-schematic@1.2.3: prismarine-schematic@1.2.3:
dependencies: dependencies:
minecraft-data: 3.98.0 minecraft-data: 3.92.0
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
@ -18268,7 +18276,7 @@ snapshots:
prismarine-windows@2.9.0: prismarine-windows@2.9.0:
dependencies: dependencies:
prismarine-item: 1.17.0 prismarine-item: 1.16.0
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
typed-emitter: 2.1.0 typed-emitter: 2.1.0
@ -18459,7 +18467,7 @@ snapshots:
puppeteer-core@2.1.1: puppeteer-core@2.1.1:
dependencies: dependencies:
'@types/mime-types': 2.1.4 '@types/mime-types': 2.1.4
debug: 4.4.1 debug: 4.4.0(supports-color@8.1.1)
extract-zip: 1.7.0 extract-zip: 1.7.0
https-proxy-agent: 4.0.0 https-proxy-agent: 4.0.0
mime: 2.6.0 mime: 2.6.0
@ -19561,8 +19569,6 @@ snapshots:
stack-generator: 2.0.10 stack-generator: 2.0.10
stacktrace-gps: 3.1.2 stacktrace-gps: 3.1.2
state-local@1.0.7: {}
static-extend@0.1.2: static-extend@0.1.2:
dependencies: dependencies:
define-property: 0.2.5 define-property: 0.2.5

View file

@ -65,7 +65,7 @@ function getAllMethods (obj) {
return [...methods] as string[] return [...methods] as string[]
} }
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => Promise<void>, chunkSize = 1) => { export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => void, chunkSize = 1) => {
// if delay is 0 then don't use setTimeout // if delay is 0 then don't use setTimeout
for (let i = 0; i < arr.length; i += chunkSize) { for (let i = 0; i < arr.length; i += chunkSize) {
if (delay) { if (delay) {
@ -74,6 +74,6 @@ export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item:
setTimeout(resolve, delay) setTimeout(resolve, delay)
}) })
} }
await exec(arr[i], i) exec(arr[i], i)
} }
} }

View file

@ -73,12 +73,12 @@ export const appAndRendererSharedConfig = () => defineConfig({
}) })
export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }: ModifyRspackConfigUtils) => { export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }: ModifyRspackConfigUtils) => {
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data|prismarine-physics/, (resource) => { appendPlugins(new rspack.NormalModuleReplacementPlugin(/data/, (resource) => {
let absolute: string let absolute: string
const request = resource.request.replaceAll('\\', '/') const request = resource.request.replaceAll('\\', '/')
absolute = path.join(resource.context, request).replaceAll('\\', '/') absolute = path.join(resource.context, request).replaceAll('\\', '/')
if (request.includes('minecraft-data/data/pc/1.') || request.includes('prismarine-physics')) { if (request.includes('minecraft-data/data/pc/1.')) {
console.log('Error: incompatible resource', request, 'from', resource.contextInfo.issuer) console.log('Error: incompatible resource', request, resource.contextInfo.issuer)
process.exit(1) process.exit(1)
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`) // throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
} }

View file

@ -48,7 +48,6 @@ export const getInitialPlayerState = () => proxy({
heldItemMain: undefined as HandItemBlock | undefined, heldItemMain: undefined as HandItemBlock | undefined,
heldItemOff: undefined as HandItemBlock | undefined, heldItemOff: undefined as HandItemBlock | undefined,
perspective: 'first_person' as CameraPerspective, perspective: 'first_person' as CameraPerspective,
onFire: false,
cameraSpectatingEntity: undefined as number | undefined, cameraSpectatingEntity: undefined as number | undefined,

View file

@ -1,55 +0,0 @@
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

@ -0,0 +1,93 @@
import { createPrismarineLightEngineWorker } from 'minecraft-lighting'
import { world } from 'prismarine-world'
// import PrismarineWorker from 'minecraft-lighting/dist/prismarineWorker.worker.js'
import { WorldDataEmitter } from './worldDataEmitter'
import { initMesherWorker, meshersSendMcData } from './worldrendererCommon'
let lightEngineNew: ReturnType<typeof createPrismarineLightEngineWorker> | null = null
export const getLightEngineSafe = () => {
// return lightEngine
return lightEngineNew
}
export const createLightEngineIfNeededNew = (worldView: WorldDataEmitter, version: string) => {
if (lightEngineNew) return
const worker = initMesherWorker((data) => {
// console.log('light engine worker message', data)
})
meshersSendMcData([worker], version)
worker.postMessage({ type: 'sideControl', value: 'lightEngine' })
lightEngineNew = createPrismarineLightEngineWorker(worker, worldView.world as unknown as world.WorldSync, loadedData)
lightEngineNew.initialize({
minY: worldView.minY,
height: worldView.minY + worldView.worldHeight,
// writeLightToOriginalWorld: true,
// enableSkyLight: false,
})
globalThis.lightEngine = lightEngineNew
}
export const processLightChunk = async (x: number, z: number, doLighting: boolean) => {
const engine = getLightEngineSafe()
if (!engine) return
const chunkX = Math.floor(x / 16)
const chunkZ = Math.floor(z / 16)
// fillColumnWithZeroLight(engine.externalWorld, chunkX, chunkZ)
const updated = await engine.loadChunk(chunkX, chunkZ, doLighting)
return updated
}
export const dumpLightData = (x: number, z: number) => {
const engine = getLightEngineSafe()
// return engine?.worldLightHolder.dumpChunk(Math.floor(x / 16), Math.floor(z / 16))
}
export const getDebugLightValues = (x: number, y: number, z: number) => {
const engine = getLightEngineSafe()
// return {
// blockLight: engine?.worldLightHolder.getBlockLight(x, y, z) ?? -1,
// skyLight: engine?.worldLightHolder.getSkyLight(x, y, z) ?? -1,
// }
}
export const updateBlockLight = async (x: number, y: number, z: number, stateId: number, distance: number) => {
if (distance > 16) return []
const chunkX = Math.floor(x / 16) * 16
const chunkZ = Math.floor(z / 16) * 16
const engine = getLightEngineSafe()
if (!engine) return
const start = performance.now()
const result = await engine.setBlock(x, y, z, stateId)
const end = performance.now()
console.log(`[light engine] updateBlockLight (${x}, ${y}, ${z}) took`, Math.round(end - start), 'ms', result.length, 'chunks')
return result
// const engine = getLightEngineSafe()
// if (!engine) return
// const affected = engine['affectedChunksTimestamps'] as Map<string, number>
// const noAffected = affected.size === 0
// engine.setBlock(x, y, z, convertPrismarineBlockToWorldBlock(stateId, loadedData))
// if (affected.size > 0) {
// const chunks = [...affected.keys()].map(key => {
// return key.split(',').map(Number) as [number, number]
// })
// affected.clear()
// return chunks
// }
}
export const lightRemoveColumn = (x: number, z: number) => {
const engine = getLightEngineSafe()
if (!engine) return
engine.unloadChunk(Math.floor(x / 16), Math.floor(z / 16))
}
export const destroyLightEngine = () => {
lightEngineNew = null
globalThis.lightEngine = null
}

View file

@ -72,7 +72,10 @@ const softCleanup = () => {
globalThis.world = world globalThis.world = world
} }
let sideControl = false
const handleMessage = data => { const handleMessage = data => {
if (sideControl) return
const globalVar: any = globalThis const globalVar: any = globalThis
if (data.type === 'mcData') { if (data.type === 'mcData') {
@ -94,6 +97,13 @@ const handleMessage = data => {
} }
switch (data.type) { switch (data.type) {
case 'sideControl': {
if (data.value === 'lightEngine') {
sideControl = true
import('minecraft-lighting/dist/prismarineWorker.worker.js')
}
break
}
case 'mesherData': { case 'mesherData': {
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu') setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
allDataReady = true allDataReady = true
@ -109,6 +119,9 @@ const handleMessage = data => {
} }
case 'chunk': { case 'chunk': {
world.addColumn(data.x, data.z, data.chunk) world.addColumn(data.x, data.z, data.chunk)
if (data.lightData) {
world.lightHolder.loadChunk(data.lightData)
}
if (data.customBlockModels) { if (data.customBlockModels) {
const chunkKey = `${data.x},${data.z}` const chunkKey = `${data.x},${data.z}`
world.customBlockModels.set(chunkKey, data.customBlockModels) world.customBlockModels.set(chunkKey, data.customBlockModels)

View file

@ -520,6 +520,7 @@ const isBlockWaterlogged = (block: Block) => {
let unknownBlockModel: BlockModelPartsResolved let unknownBlockModel: BlockModelPartsResolved
export function getSectionGeometry (sx: number, sy: number, sz: number, world: World) { export function getSectionGeometry (sx: number, sy: number, sz: number, world: World) {
world.hadSkyLight = false
let delayedRender = [] as Array<() => void> let delayedRender = [] as Array<() => void>
const attr: MesherGeometryOutput = { const attr: MesherGeometryOutput = {
@ -716,6 +717,8 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
delete attr.uvs delete attr.uvs
} }
attr.hasSkylight = world.hadSkyLight
return attr return attr
} }

View file

@ -8,6 +8,9 @@ export const defaultMesherConfig = {
enableLighting: true, enableLighting: true,
skyLight: 15, skyLight: 15,
smoothLighting: true, smoothLighting: true,
usingCustomLightHolder: false,
flyingSquidWorkarounds: false,
outputFormat: 'threeJs' as 'threeJs' | 'webgpu', outputFormat: 'threeJs' as 'threeJs' | 'webgpu',
// textureSize: 1024, // for testing // textureSize: 1024, // for testing
debugModelVariant: undefined as undefined | number[], debugModelVariant: undefined as undefined | number[],
@ -45,6 +48,7 @@ export type MesherGeometryOutput = {
hadErrors: boolean hadErrors: boolean
blocksCount: number blocksCount: number
customBlockModels?: CustomBlockModels customBlockModels?: CustomBlockModels
hasSkylight?: boolean
} }
export interface MesherMainEvents { export interface MesherMainEvents {

View file

@ -1,3 +1,4 @@
import { WorldLightHolder } from 'minecraft-lighting/dist/worldLightHolder'
import Chunks from 'prismarine-chunk' import Chunks from 'prismarine-chunk'
import mcData from 'minecraft-data' import mcData from 'minecraft-data'
import { Block } from 'prismarine-block' import { Block } from 'prismarine-block'
@ -32,6 +33,8 @@ export type WorldBlock = Omit<Block, 'position'> & {
} }
export class World { export class World {
hadSkyLight = false
lightHolder = new WorldLightHolder(0, 0)
config = defaultMesherConfig config = defaultMesherConfig
Chunk: typeof import('prismarine-chunk/types/index').PCChunk Chunk: typeof import('prismarine-chunk/types/index').PCChunk
columns = {} as { [key: string]: import('prismarine-chunk/types/index').PCChunk } columns = {} as { [key: string]: import('prismarine-chunk/types/index').PCChunk }
@ -53,38 +56,71 @@ export class World {
getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') { getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') {
// for easier testing // for easier testing
if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number]) if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
const { enableLighting, skyLight } = this.config
const IS_USING_LOCAL_SERVER_LIGHTING = this.config.flyingSquidWorkarounds
// const IS_USING_SERVER_LIGHTING = false
const { enableLighting, skyLight, usingCustomLightHolder } = this.config
if (!enableLighting) return 15 if (!enableLighting) return 15
// const key = `${pos.x},${pos.y},${pos.z}`
// if (lightsCache.has(key)) return lightsCache.get(key)
const column = this.getColumnByPos(pos) const column = this.getColumnByPos(pos)
if (!column || !hasChunkSection(column, pos)) return 15 if (!column) return 15
let result = Math.min( if (!usingCustomLightHolder && !hasChunkSection(column, pos)) return 2
15, let result = Math.max(
Math.max( 2,
column.getBlockLight(posInChunk(pos)), Math.min(
Math.min(skyLight, column.getSkyLight(posInChunk(pos))) 15,
) + 2 Math.max(
this.getBlockLight(pos),
Math.min(skyLight, this.getSkyLight(pos))
)
)
) )
// lightsCache.set(key, result) if (result === 2 && IS_USING_LOCAL_SERVER_LIGHTING) {
if (result === 2 && [this.getBlock(pos)?.name ?? '', curBlockName].some(x => /_stairs|slab|glass_pane/.exec(x)) && !skipMoreChecks) { // todo this is obviously wrong if ([this.getBlock(pos)?.name ?? '', curBlockName].some(x => /_stairs|slab|glass_pane/.exec(x)) && !skipMoreChecks) { // todo this is obviously wrong
const lights = [ const lights = [
this.getLight(pos.offset(0, 1, 0), undefined, true), this.getLight(pos.offset(0, 1, 0), undefined, true),
this.getLight(pos.offset(0, -1, 0), undefined, true), this.getLight(pos.offset(0, -1, 0), undefined, true),
this.getLight(pos.offset(0, 0, 1), undefined, true), this.getLight(pos.offset(0, 0, 1), undefined, true),
this.getLight(pos.offset(0, 0, -1), undefined, true), this.getLight(pos.offset(0, 0, -1), undefined, true),
this.getLight(pos.offset(1, 0, 0), undefined, true), this.getLight(pos.offset(1, 0, 0), undefined, true),
this.getLight(pos.offset(-1, 0, 0), undefined, true) this.getLight(pos.offset(-1, 0, 0), undefined, true)
].filter(x => x !== 2) ].filter(x => x !== 2)
if (lights.length) { if (lights.length) {
const min = Math.min(...lights) const min = Math.min(...lights)
result = min result = min
}
} }
if (isNeighbor) result = 15 // TODO
} }
if (isNeighbor && result === 2) result = 15 // TODO
return result return result
} }
getBlockLight (pos: Vec3) {
// if (this.config.clientSideLighting) {
// return this.lightHolder.getBlockLight(pos.x, pos.y, pos.z)
// }
const column = this.getColumnByPos(pos)
if (!column) return 15
return column.getBlockLight(posInChunk(pos))
}
getSkyLight (pos: Vec3) {
const result = this.getSkyLightInner(pos)
if (result > 2) this.hadSkyLight = true
return result
}
getSkyLightInner (pos: Vec3) {
// if (this.config.clientSideLighting) {
// return this.lightHolder.getSkyLight(pos.x, pos.y, pos.z)
// }
const column = this.getColumnByPos(pos)
if (!column) return 15
return column.getSkyLight(posInChunk(pos))
}
addColumn (x, z, json) { addColumn (x, z, json) {
const chunk = this.Chunk.fromJson(json) const chunk = this.Chunk.fromJson(json)
this.columns[columnKey(x, z)] = chunk as any this.columns[columnKey(x, z)] = chunk as any

View file

@ -7,9 +7,10 @@ import { Vec3 } from 'vec3'
import { BotEvents } from 'mineflayer' import { BotEvents } from 'mineflayer'
import { proxy } from 'valtio' import { proxy } from 'valtio'
import TypedEmitter from 'typed-emitter' import TypedEmitter from 'typed-emitter'
import { Biome } from 'minecraft-data'
import { delayedIterator } from '../../playground/shared' import { delayedIterator } from '../../playground/shared'
import { chunkPos } from './simpleUtils' import { chunkPos } from './simpleUtils'
import { createLightEngineIfNeededNew, destroyLightEngine, lightRemoveColumn, processLightChunk, updateBlockLight } from './lightEngine'
import { WorldRendererConfig } from './worldrendererCommon'
export type ChunkPosKey = string // like '16,16' export type ChunkPosKey = string // like '16,16'
type ChunkPos = { x: number, z: number } // like { x: 16, z: 16 } type ChunkPos = { x: number, z: number } // like { x: 16, z: 16 }
@ -29,24 +30,24 @@ export type WorldDataEmitterEvents = {
updateLight: (data: { pos: Vec3 }) => void updateLight: (data: { pos: Vec3 }) => void
onWorldSwitch: () => void onWorldSwitch: () => void
end: () => void end: () => void
biomeUpdate: (data: { biome: Biome }) => void
biomeReset: () => void
} }
export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) { export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
static readonly restorerName = 'WorldDataEmitterWorker' static readonly restorerName = 'WorldDataEmitterWorker'
destroy () {
this.removeAllListeners()
}
} }
export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) { export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
spiralNumber = 0 minY = -64
gotPanicLastTime = false worldHeight = 384
panicChunksReload = () => {} dimensionName = ''
version = ''
worldRendererConfig: WorldRendererConfig
loadedChunks: Record<ChunkPosKey, boolean> loadedChunks: Record<ChunkPosKey, boolean>
private inLoading = false
private chunkReceiveTimes: number[] = []
private lastChunkReceiveTime = 0
public lastChunkReceiveTimeAvg = 0
private panicTimeout?: NodeJS.Timeout
readonly lastPos: Vec3 readonly lastPos: Vec3
private eventListeners: Record<string, any> = {} private eventListeners: Record<string, any> = {}
private readonly emitter: WorldDataEmitter private readonly emitter: WorldDataEmitter
@ -75,18 +76,22 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
this.emitter = this this.emitter = this
} }
setBlockStateId (position: Vec3, stateId: number) { // setBlockStateId (position: Vec3, stateId: number) {
const val = this.world.setBlockStateId(position, stateId) as Promise<void> | void // const val = this.world.setBlockStateId(position, stateId) as Promise<void> | void
if (val) throw new Error('setBlockStateId returned promise (not supported)') // if (val) throw new Error('setBlockStateId returned promise (not supported)')
// const chunkX = Math.floor(position.x / 16) // // const chunkX = Math.floor(position.x / 16)
// const chunkZ = Math.floor(position.z / 16) // // const chunkZ = Math.floor(position.z / 16)
// if (!this.loadedChunks[`${chunkX},${chunkZ}`] && !this.waitingSpiralChunksLoad[`${chunkX},${chunkZ}`]) { // // if (!this.loadedChunks[`${chunkX},${chunkZ}`] && !this.waitingSpiralChunksLoad[`${chunkX},${chunkZ}`]) {
// void this.loadChunk({ x: chunkX, z: chunkZ }) // // void this.loadChunk({ x: chunkX, z: chunkZ })
// return // // return
// } // // }
this.emit('blockUpdate', { pos: position, stateId }) // const updateChunks = this.worldRendererConfig.clientSideLighting ? updateBlockLight(position.x, position.y, position.z, stateId) ?? [] : []
} // this.emit('blockUpdate', { pos: position, stateId })
// for (const chunk of updateChunks) {
// void this.loadChunk(new Vec3(chunk[0] * 16, 0, chunk[1] * 16), true, 'setBlockStateId light update')
// }
// }
updateViewDistance (viewDistance: number) { updateViewDistance (viewDistance: number) {
this.viewDistance = viewDistance this.viewDistance = viewDistance
@ -94,6 +99,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
} }
listenToBot (bot: typeof __type_bot) { listenToBot (bot: typeof __type_bot) {
this.version = bot.version
const entitiesObjectData = new Map<string, number>() const entitiesObjectData = new Map<string, number>()
bot._client.prependListener('spawn_entity', (data) => { bot._client.prependListener('spawn_entity', (data) => {
if (data.objectData && data.entityId !== undefined) { if (data.objectData && data.entityId !== undefined) {
@ -144,26 +150,26 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
this.emitter.emit('entity', { id: e.id, delete: true }) this.emitter.emit('entity', { id: e.id, delete: true })
}, },
chunkColumnLoad: (pos: Vec3) => { chunkColumnLoad: (pos: Vec3) => {
const now = performance.now()
if (this.lastChunkReceiveTime) {
this.chunkReceiveTimes.push(now - this.lastChunkReceiveTime)
}
this.lastChunkReceiveTime = now
if (this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`]) { if (this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`]) {
this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`](true) this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`](true)
delete this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`] delete this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`]
} else if (this.loadedChunks[`${pos.x},${pos.z}`]) { } else if (this.loadedChunks[`${pos.x},${pos.z}`]) {
void this.loadChunk(pos, false, 'Received another chunkColumnLoad event while already loaded') void this.loadChunk(pos, false, 'Received another chunkColumnLoad event while already loaded')
} }
this.chunkProgress()
}, },
chunkColumnUnload: (pos: Vec3) => { chunkColumnUnload: (pos: Vec3) => {
this.unloadChunk(pos) this.unloadChunk(pos)
}, },
blockUpdate: (oldBlock: any, newBlock: any) => { blockUpdate: async (oldBlock, newBlock) => {
if (typeof newBlock.stateId === 'number' && oldBlock?.stateId === newBlock.stateId) return
const stateId = newBlock.stateId ?? ((newBlock.type << 4) | newBlock.metadata) const stateId = newBlock.stateId ?? ((newBlock.type << 4) | newBlock.metadata)
this.emitter.emit('blockUpdate', { pos: oldBlock.position, stateId }) const distance = newBlock.position.distanceTo(this.lastPos)
this.emit('blockUpdate', { pos: newBlock.position, stateId })
const updateChunks = this.worldRendererConfig.clientSideLighting === 'none' ? [] : await updateBlockLight(newBlock.position.x, newBlock.position.y, newBlock.position.z, stateId, distance) ?? []
for (const chunk of updateChunks) {
void this.loadChunk(new Vec3(chunk.chunkX * 16, 0, chunk.chunkZ * 16), true, 'setBlockStateId light update')
}
}, },
time: () => { time: () => {
this.emitter.emit('time', bot.time.timeOfDay) this.emitter.emit('time', bot.time.timeOfDay)
@ -172,17 +178,22 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
this.emitter.emit('end') this.emitter.emit('end')
}, },
// when dimension might change // when dimension might change
login: () => { login () {
void this.updatePosition(bot.entity.position, true) possiblyDimensionChange()
this.emitter.emit('playerEntity', bot.entity)
}, },
respawn: () => { respawn: () => {
void this.updatePosition(bot.entity.position, true) possiblyDimensionChange()
this.emitter.emit('playerEntity', bot.entity)
this.emitter.emit('onWorldSwitch') this.emitter.emit('onWorldSwitch')
}, },
} satisfies Partial<BotEvents> } satisfies Partial<BotEvents>
const possiblyDimensionChange = () => {
this.minY = bot.game['minY'] ?? -64
this.worldHeight = bot.game['height'] ?? 384
this.dimensionName = bot.game['dimension'] ?? ''
void this.updatePosition(bot.entity.position, true)
this.emitter.emit('playerEntity', bot.entity)
}
bot._client.on('update_light', ({ chunkX, chunkZ }) => { bot._client.on('update_light', ({ chunkX, chunkZ }) => {
const chunkPos = new Vec3(chunkX * 16, 0, chunkZ * 16) const chunkPos = new Vec3(chunkX * 16, 0, chunkZ * 16)
@ -222,6 +233,14 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
} }
} }
destroy () {
if (bot) {
this.removeListenersFromBot(bot as any)
}
this.emitter.removeAllListeners()
destroyLightEngine()
}
async init (pos: Vec3) { async init (pos: Vec3) {
this.updateViewDistance(this.viewDistance) this.updateViewDistance(this.viewDistance)
this.emitter.emit('chunkPosUpdate', { pos }) this.emitter.emit('chunkPosUpdate', { pos })
@ -237,59 +256,33 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16)) const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16))
this.lastPos.update(pos) this.lastPos.update(pos)
await this._loadChunks(positions, pos) await this._loadChunks(positions)
} }
chunkProgress () { async _loadChunks (positions: Vec3[], sliceSize = 5) {
if (this.panicTimeout) clearTimeout(this.panicTimeout)
if (this.chunkReceiveTimes.length >= 5) {
const avgReceiveTime = this.chunkReceiveTimes.reduce((a, b) => a + b, 0) / this.chunkReceiveTimes.length
this.lastChunkReceiveTimeAvg = avgReceiveTime
const timeoutDelay = avgReceiveTime * 2 + 1000 // 2x average + 1 second
// Clear any existing timeout
if (this.panicTimeout) clearTimeout(this.panicTimeout)
// Set new timeout for panic reload
this.panicTimeout = setTimeout(() => {
if (!this.gotPanicLastTime && this.inLoading) {
console.warn('Chunk loading seems stuck, triggering panic reload')
this.gotPanicLastTime = true
this.panicChunksReload()
}
}, timeoutDelay)
}
}
async _loadChunks (positions: Vec3[], centerPos: Vec3) {
this.spiralNumber++
const { spiralNumber } = this
// stop loading previous chunks // stop loading previous chunks
for (const pos of Object.keys(this.waitingSpiralChunksLoad)) { for (const pos of Object.keys(this.waitingSpiralChunksLoad)) {
this.waitingSpiralChunksLoad[pos](false) this.waitingSpiralChunksLoad[pos](false)
delete this.waitingSpiralChunksLoad[pos] delete this.waitingSpiralChunksLoad[pos]
} }
const promises = [] as Array<Promise<void>>
let continueLoading = true let continueLoading = true
this.inLoading = true
await delayedIterator(positions, this.addWaitTime, async (pos) => { await delayedIterator(positions, this.addWaitTime, async (pos) => {
if (!continueLoading || this.loadedChunks[`${pos.x},${pos.z}`]) return const promise = (async () => {
if (!continueLoading || this.loadedChunks[`${pos.x},${pos.z}`]) return
// Wait for chunk to be available from server if (!this.world.getColumnAt(pos)) {
if (!this.world.getColumnAt(pos)) { continueLoading = await new Promise<boolean>(resolve => {
continueLoading = await new Promise<boolean>(resolve => { this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`] = resolve
this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`] = resolve })
}) }
} if (!continueLoading) return
if (!continueLoading) return await this.loadChunk(pos)
await this.loadChunk(pos, undefined, `spiral ${spiralNumber} from ${centerPos.x},${centerPos.z}`) })()
this.chunkProgress() promises.push(promise)
}) })
if (this.panicTimeout) clearTimeout(this.panicTimeout) await Promise.all(promises)
this.inLoading = false
this.gotPanicLastTime = false
this.chunkReceiveTimes = []
this.lastChunkReceiveTime = 0
} }
readdDebug () { readdDebug () {
@ -312,14 +305,33 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
// lastTime = 0 // lastTime = 0
async loadChunk (pos: ChunkPos, isLightUpdate = false, reason = 'spiral') { async loadChunk (pos: ChunkPos, isLightUpdate = false, reason = 'spiral') {
const [botX, botZ] = chunkPos(this.lastPos) createLightEngineIfNeededNew(this, this.version)
const dx = Math.abs(botX - Math.floor(pos.x / 16)) const [botX, botZ] = chunkPos(this.lastPos)
const dz = Math.abs(botZ - Math.floor(pos.z / 16)) const chunkX = Math.floor(pos.x / 16)
const chunkZ = Math.floor(pos.z / 16)
const dx = Math.abs(botX - chunkX)
const dz = Math.abs(botZ - chunkZ)
if (dx <= this.viewDistance && dz <= this.viewDistance) { if (dx <= this.viewDistance && dz <= this.viewDistance) {
// eslint-disable-next-line @typescript-eslint/await-thenable -- todo allow to use async world provider but not sure if needed // eslint-disable-next-line @typescript-eslint/await-thenable -- todo allow to use async world provider but not sure if needed
const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z)) const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z))
if (column) { if (column) {
let result = [] as Array<{ chunkX: number, chunkZ: number }>
if (!isLightUpdate) {
const computeLighting = this.worldRendererConfig.clientSideLighting === 'full'
const promise = processLightChunk(pos.x, pos.z, computeLighting)
if (computeLighting) {
result = (await promise) ?? []
}
}
if (!result) return
for (const affectedChunk of result) {
if (affectedChunk.chunkX === chunkX && affectedChunk.chunkZ === chunkZ) continue
const loadedChunk = this.loadedChunks[`${affectedChunk.chunkX * 16},${affectedChunk.chunkZ * 16}`]
if (!loadedChunk) continue
void this.loadChunk(new Vec3(affectedChunk.chunkX * 16, 0, affectedChunk.chunkZ * 16), true)
}
// const latency = Math.floor(performance.now() - this.lastTime) // const latency = Math.floor(performance.now() - this.lastTime)
// this.debugGotChunkLatency.push(latency) // this.debugGotChunkLatency.push(latency)
// this.lastTime = performance.now() // this.lastTime = performance.now()
@ -361,39 +373,11 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
this.emitter.emit('unloadChunk', { x: pos.x, z: pos.z }) this.emitter.emit('unloadChunk', { x: pos.x, z: pos.z })
delete this.loadedChunks[`${pos.x},${pos.z}`] delete this.loadedChunks[`${pos.x},${pos.z}`]
delete this.debugChunksInfo[`${pos.x},${pos.z}`] delete this.debugChunksInfo[`${pos.x},${pos.z}`]
lightRemoveColumn(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) { async updatePosition (pos: Vec3, force = false) {
if (!this.allowPositionUpdate) return 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 [lastX, lastZ] = chunkPos(this.lastPos)
const [botX, botZ] = chunkPos(pos) const [botX, botZ] = chunkPos(pos)
if (lastX !== botX || lastZ !== botZ || force) { if (lastX !== botX || lastZ !== botZ || force) {
@ -411,6 +395,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
chunksToUnload.push(p) chunksToUnload.push(p)
} }
} }
console.log('unloading', chunksToUnload.length, 'total now', Object.keys(this.loadedChunks).length)
for (const p of chunksToUnload) { for (const p of chunksToUnload) {
this.unloadChunk(p) this.unloadChunk(p)
} }
@ -422,7 +407,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
return undefined! return undefined!
}).filter(a => !!a) }).filter(a => !!a)
this.lastPos.update(pos) this.lastPos.update(pos)
void this._loadChunks(positions, pos) void this._loadChunks(positions)
} else { } else {
this.emitter.emit('chunkPosUpdate', { pos }) // todo-low this.emitter.emit('chunkPosUpdate', { pos }) // todo-low
this.lastPos.update(pos) this.lastPos.update(pos)

View file

@ -15,6 +15,7 @@ import { buildCleanupDecorator } from './cleanupDecorator'
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared' import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared'
import { chunkPos } from './simpleUtils' import { chunkPos } from './simpleUtils'
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats' import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
import { dumpLightData } from './lightEngine'
import { WorldDataEmitterWorker } from './worldDataEmitter' import { WorldDataEmitterWorker } from './worldDataEmitter'
import { getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from './basePlayerState' import { getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from './basePlayerState'
import { MesherLogReader } from './mesherlogReader' import { MesherLogReader } from './mesherlogReader'
@ -32,50 +33,40 @@ const toMajorVersion = version => {
export const worldCleanup = buildCleanupDecorator('resetWorld') export const worldCleanup = buildCleanupDecorator('resetWorld')
export const defaultWorldRendererConfig = { export const defaultWorldRendererConfig = {
// Debug settings
showChunkBorders: false, showChunkBorders: false,
enableDebugOverlay: false,
// Performance settings
mesherWorkers: 4, mesherWorkers: 4,
addChunksBatchWaitTime: 200, isPlayground: false,
_experimentalSmoothChunkLoading: true, renderEars: true,
_renderByChunks: false, skinTexturesProxy: undefined as string | undefined,
// game renderer setting actually
// Rendering engine settings
dayCycle: true,
smoothLighting: true,
enableLighting: true,
starfield: true,
defaultSkybox: true,
renderEntities: true,
extraBlockRenderers: true,
foreground: true,
fov: 75,
volume: 1,
// Camera visual related settings
showHand: false, showHand: false,
viewBobbing: false, viewBobbing: false,
renderEars: true, extraBlockRenderers: true,
highlightBlockColor: 'blue', clipWorldBelowY: undefined as number | undefined,
smoothLighting: true,
// Player models enableLighting: true,
fetchPlayerSkins: true, legacyLighting: false,
skinTexturesProxy: undefined as string | undefined, clientSideLighting: 'full' as 'full' | 'partial' | 'none',
flyingSquidWorkarounds: false,
// VR settings starfield: true,
addChunksBatchWaitTime: 200,
vrSupport: true, vrSupport: true,
vrPageGameRendering: true, vrPageGameRendering: true,
renderEntities: true,
// World settings fov: 75,
clipWorldBelowY: undefined as number | undefined, fetchPlayerSkins: true,
isPlayground: false highlightBlockColor: 'blue',
foreground: true,
enableDebugOverlay: false,
_experimentalSmoothChunkLoading: true,
_renderByChunks: false,
volume: 1
} }
export type WorldRendererConfig = typeof defaultWorldRendererConfig export type WorldRendererConfig = typeof defaultWorldRendererConfig
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> { export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
skyLight = 15
worldReadyResolvers = Promise.withResolvers<void>() worldReadyResolvers = Promise.withResolvers<void>()
worldReadyPromise = this.worldReadyResolvers.promise worldReadyPromise = this.worldReadyResolvers.promise
timeOfTheDay = 0 timeOfTheDay = 0
@ -510,9 +501,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
timeUpdated? (newTime: number): void timeUpdated? (newTime: number): void
biomeUpdated? (biome: any): void skylightUpdated? (): void
biomeReset? (): void
updateViewerPosition (pos: Vec3) { updateViewerPosition (pos: Vec3) {
this.viewerChunkPosition = pos this.viewerChunkPosition = pos
@ -561,7 +550,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.sendMesherMcData() this.sendMesherMcData()
} }
getMesherConfig (): MesherConfig { changeSkyLight () {
let skyLight = 15 let skyLight = 15
const timeOfDay = this.timeOfTheDay const timeOfDay = this.timeOfTheDay
if (timeOfDay < 0 || timeOfDay > 24_000) { if (timeOfDay < 0 || timeOfDay > 24_000) {
@ -574,34 +563,35 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
skyLight = ((timeOfDay - 12_000) / 6000) * 15 skyLight = ((timeOfDay - 12_000) / 6000) * 15
} }
skyLight = Math.floor(skyLight) this.skyLight = Math.floor(skyLight)
}
getMesherConfig (): MesherConfig {
return { return {
version: this.version, version: this.version,
enableLighting: this.worldRendererConfig.enableLighting, enableLighting: this.worldRendererConfig.enableLighting && !this.playerStateReactive.lightingDisabled,
skyLight, skyLight: this.skyLight,
smoothLighting: this.worldRendererConfig.smoothLighting, smoothLighting: this.worldRendererConfig.smoothLighting,
outputFormat: this.outputFormat, outputFormat: this.outputFormat,
// textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width, // textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
debugModelVariant: undefined, debugModelVariant: undefined,
clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY, clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY,
disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers, disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers,
usingCustomLightHolder: false,
flyingSquidWorkarounds: this.worldRendererConfig.flyingSquidWorkarounds,
worldMinY: this.worldMinYRender, worldMinY: this.worldMinYRender,
worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight, worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight,
} }
} }
sendMesherMcData () { sendMesherMcData () {
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajorVersion(this.version)] meshersSendMcData(
const mcData = { this.workers,
version: JSON.parse(JSON.stringify(allMcData.version)) this.version,
} {
for (const key of dynamicMcDataFiles) { config: this.getMesherConfig()
mcData[key] = allMcData[key] }
} )
for (const worker of this.workers) {
worker.postMessage({ type: 'mcData', mcData, config: this.getMesherConfig() })
}
this.logWorkerWork('# mcData sent') this.logWorkerWork('# mcData sent')
} }
@ -659,7 +649,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
x, x,
z, z,
chunk, chunk,
customBlockModels: customBlockModels || undefined customBlockModels: customBlockModels || undefined,
lightData: dumpLightData(x, z)
}) })
} }
this.workers[0].postMessage({ this.workers[0].postMessage({
@ -835,24 +826,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}) })
worldEmitter.on('time', (timeOfDay) => { worldEmitter.on('time', (timeOfDay) => {
if (!this.worldRendererConfig.dayCycle) return if (timeOfDay < 0 || timeOfDay > 24_000) {
this.timeUpdated?.(timeOfDay) throw new Error('Invalid time of day. It should be between 0 and 24000.')
}
const oldSkyLight = this.skyLight
this.timeOfTheDay = timeOfDay this.timeOfTheDay = timeOfDay
this.changeSkyLight()
// if (this.worldRendererConfig.skyLight === skyLight) return if (oldSkyLight !== this.skyLight) {
// this.worldRendererConfig.skyLight = skyLight this.skylightUpdated?.()
// if (this instanceof WorldRendererThree) { }
// (this).rerenderAllChunks?.() this.timeUpdated?.(timeOfDay)
// }
})
worldEmitter.on('biomeUpdate', ({ biome }) => {
this.biomeUpdated?.(biome)
})
worldEmitter.on('biomeReset', () => {
this.biomeReset?.()
}) })
} }
@ -945,7 +929,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.reactiveState.world.mesherWork = true this.reactiveState.world.mesherWork = true
const distance = this.getDistance(pos) const distance = this.getDistance(pos)
// todo shouldnt we check loadedChunks instead? // todo shouldnt we check loadedChunks instead?
if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return // if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}` const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
// if (this.sectionsOutstanding.has(key)) return // if (this.sectionsOutstanding.has(key)) return
this.renderUpdateEmitter.emit('dirty', pos, value) this.renderUpdateEmitter.emit('dirty', pos, value)
@ -1049,12 +1033,16 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.renderUpdateEmitter.removeAllListeners() this.renderUpdateEmitter.removeAllListeners()
this.abortController.abort() this.abortController.abort()
removeAllStats() removeAllStats()
this.displayOptions.worldView.destroy()
} }
} }
export const initMesherWorker = (onGotMessage: (data: any) => void) => { export const initMesherWorker = (onGotMessage: (data: any) => void) => {
// Node environment needs an absolute path, but browser needs the url of the file // Node environment needs an absolute path, but browser needs the url of the file
const workerName = 'mesher.js' const workerName = 'mesher.js'
// eslint-disable-next-line node/no-path-concat
const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName
let worker: any let worker: any
if (process.env.SINGLE_FILE_BUILD) { if (process.env.SINGLE_FILE_BUILD) {
@ -1062,7 +1050,7 @@ export const initMesherWorker = (onGotMessage: (data: any) => void) => {
const blob = new Blob([workerCode], { type: 'text/javascript' }) const blob = new Blob([workerCode], { type: 'text/javascript' })
worker = new Worker(window.URL.createObjectURL(blob)) worker = new Worker(window.URL.createObjectURL(blob))
} else { } else {
worker = new Worker(workerName) worker = new Worker(src)
} }
worker.onmessage = ({ data }) => { worker.onmessage = ({ data }) => {

View file

@ -80,12 +80,8 @@ export class CameraShake {
camera.setRotationFromQuaternion(yawQuat) camera.setRotationFromQuaternion(yawQuat)
} else { } else {
// For regular camera, apply all rotations // For regular camera, apply all rotations
// Add tiny offsets to prevent z-fighting at ideal angles (90, 180, 270 degrees) const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.basePitch)
const pitchOffset = this.addAntiZfightingOffset(this.basePitch) const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw)
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)) 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 // Combine rotations in the correct order: pitch -> yaw -> roll
const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat) const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat)
@ -100,21 +96,4 @@ export class CameraShake {
private easeInOut (t: number): number { private easeInOut (t: number): number {
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2 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

@ -61,9 +61,8 @@ export class DocumentRenderer {
this.previousCanvasWidth = this.canvas.width this.previousCanvasWidth = this.canvas.width
this.previousCanvasHeight = this.canvas.height this.previousCanvasHeight = this.canvas.height
const supportsWebGL2 = 'WebGL2RenderingContext' in window
// Only initialize stats and DOM-related features in main thread // Only initialize stats and DOM-related features in main thread
if (!externalCanvas && supportsWebGL2) { if (!externalCanvas) {
this.stats = new TopRightStats(this.canvas as HTMLCanvasElement, this.config.statsVisible) this.stats = new TopRightStats(this.canvas as HTMLCanvasElement, this.config.statsVisible)
this.setupFpsTracking() this.setupFpsTracking()
} }

View file

@ -20,9 +20,7 @@ import { ItemSpecificContextProperties } from '../lib/basePlayerState'
import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins' import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins'
import { renderComponent } from '../sign-renderer' import { renderComponent } from '../sign-renderer'
import { createCanvas } from '../lib/utils' import { createCanvas } from '../lib/utils'
import { PlayerObjectType } from '../lib/createPlayerObject'
import { getBlockMeshFromModel } from './holdingBlock' import { getBlockMeshFromModel } from './holdingBlock'
import { createItemMesh } from './itemMesh'
import * as Entity from './entity/EntityMesh' import * as Entity from './entity/EntityMesh'
import { getMesh } from './entity/EntityMesh' import { getMesh } from './entity/EntityMesh'
import { WalkingGeneralSwing } from './entity/animations' import { WalkingGeneralSwing } from './entity/animations'
@ -34,6 +32,12 @@ export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
export const TWEEN_DURATION = 120 export const TWEEN_DURATION = 120
type PlayerObjectType = PlayerObject & {
animation?: PlayerAnimation
realPlayerUuid: string
realUsername: string
}
function convert2sComplementToHex (complement: number) { function convert2sComplementToHex (complement: number) {
if (complement < 0) { if (complement < 0) {
complement = (0xFF_FF_FF_FF + complement + 1) >>> 0 complement = (0xFF_FF_FF_FF + complement + 1) >>> 0
@ -137,7 +141,7 @@ const addNametag = (entity, options: { fontFamily: string }, mesh, version: stri
const canvas = getUsernameTexture(entity, options, version) const canvas = getUsernameTexture(entity, options, version)
const tex = new THREE.Texture(canvas) const tex = new THREE.Texture(canvas)
tex.needsUpdate = true tex.needsUpdate = true
let nameTag: THREE.Object3D let nameTag
if (entity.nameTagFixed) { if (entity.nameTagFixed) {
const geometry = new THREE.PlaneGeometry() const geometry = new THREE.PlaneGeometry()
const material = new THREE.MeshBasicMaterial({ map: tex }) const material = new THREE.MeshBasicMaterial({ map: tex })
@ -167,7 +171,6 @@ const addNametag = (entity, options: { fontFamily: string }, mesh, version: stri
nameTag.name = 'nametag' nameTag.name = 'nametag'
mesh.add(nameTag) mesh.add(nameTag)
return nameTag
} }
} }
@ -491,10 +494,6 @@ export class Entities {
// todo true/undefined doesnt reset the skin to the default one // todo true/undefined doesnt reset the skin to the default one
// eslint-disable-next-line max-params // eslint-disable-next-line max-params
async updatePlayerSkin (entityId: string | number, username: string | undefined, uuidCache: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) { async updatePlayerSkin (entityId: string | number, username: string | undefined, uuidCache: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) {
const isCustomSkin = skinUrl !== stevePngUrl
if (isCustomSkin) {
this.loadedSkinEntityIds.add(String(entityId))
}
if (uuidCache) { if (uuidCache) {
if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache] = {} if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache] = {}
if (typeof skinUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].skinUrl = skinUrl if (typeof skinUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].skinUrl = skinUrl
@ -718,7 +717,7 @@ export class Entities {
return typeof component === 'string' ? component : component.text ?? '' return typeof component === 'string' ? component : component.text ?? ''
} }
getItemMesh (item, specificProps: ItemSpecificContextProperties, faceCamera = false, previousModel?: string) { getItemMesh (item, specificProps: ItemSpecificContextProperties, previousModel?: string) {
if (!item.nbt && item.nbtData) item.nbt = item.nbtData if (!item.nbt && item.nbtData) item.nbt = item.nbtData
const textureUv = this.worldRenderer.getItemRenderData(item, specificProps) const textureUv = this.worldRenderer.getItemRenderData(item, specificProps)
if (previousModel && previousModel === textureUv?.modelName) return undefined if (previousModel && previousModel === textureUv?.modelName) return undefined
@ -737,41 +736,60 @@ export class Entities {
return { return {
mesh: outerGroup, mesh: outerGroup,
isBlock: true, isBlock: true,
itemsTexture: null,
itemsTextureFlipped: null,
modelName: textureUv.modelName, modelName: textureUv.modelName,
} }
} }
// Render proper 3D model for items // TODO: Render proper model (especially for blocks) instead of flat texture
if (textureUv) { if (textureUv) {
const textureThree = textureUv.renderInfo?.texture === 'blocks' ? this.worldRenderer.material.map! : this.worldRenderer.itemsTexture const textureThree = textureUv.renderInfo?.texture === 'blocks' ? this.worldRenderer.material.map! : this.worldRenderer.itemsTexture
// todo use geometry buffer uv instead!
const { u, v, su, sv } = textureUv const { u, v, su, sv } = textureUv
const sizeX = su ?? 1 // su is actually width const size = undefined
const sizeY = sv ?? 1 // sv is actually height const itemsTexture = textureThree.clone()
itemsTexture.flipY = true
// Use the new unified item mesh function const sizeY = (sv ?? size)!
const result = createItemMesh(textureThree, { const sizeX = (su ?? size)!
u, itemsTexture.offset.set(u, 1 - v - sizeY)
v, itemsTexture.repeat.set(sizeX, sizeY)
sizeX, itemsTexture.needsUpdate = true
sizeY itemsTexture.magFilter = THREE.NearestFilter
}, { itemsTexture.minFilter = THREE.NearestFilter
faceCamera, const itemsTextureFlipped = itemsTexture.clone()
use3D: !faceCamera, // Only use 3D for non-camera-facing items itemsTextureFlipped.repeat.x *= -1
itemsTextureFlipped.needsUpdate = true
itemsTextureFlipped.offset.set(u + (sizeX), 1 - v - sizeY)
const material = new THREE.MeshStandardMaterial({
map: itemsTexture,
transparent: true,
alphaTest: 0.1,
}) })
const materialFlipped = new THREE.MeshStandardMaterial({
map: itemsTextureFlipped,
transparent: true,
alphaTest: 0.1,
})
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
// top left and right bottom are black box materials others are transparent
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
material, materialFlipped,
])
let SCALE = 1 let SCALE = 1
if (specificProps['minecraft:display_context'] === 'ground') { if (specificProps['minecraft:display_context'] === 'ground') {
SCALE = 0.5 SCALE = 0.5
} else if (specificProps['minecraft:display_context'] === 'thirdperson') { } else if (specificProps['minecraft:display_context'] === 'thirdperson') {
SCALE = 6 SCALE = 6
} }
result.mesh.scale.set(SCALE, SCALE, SCALE) mesh.scale.set(SCALE, SCALE, SCALE)
return { return {
mesh: result.mesh, mesh,
isBlock: false, isBlock: false,
itemsTexture,
itemsTextureFlipped,
modelName: textureUv.modelName, modelName: textureUv.modelName,
cleanup: result.cleanup
} }
} }
} }
@ -787,6 +805,8 @@ export class Entities {
} }
update (entity: SceneEntity['originalEntity'], overrides) { update (entity: SceneEntity['originalEntity'], overrides) {
const justAdded = !this.entities[entity.id]
const isPlayerModel = entity.name === 'player' const isPlayerModel = entity.name === 'player'
if (entity.name === 'zombie_villager' || entity.name === 'husk') { if (entity.name === 'zombie_villager' || entity.name === 'husk') {
overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}` overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}`
@ -797,7 +817,6 @@ export class Entities {
} }
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted) // this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
let e = this.entities[entity.id] let e = this.entities[entity.id]
const justAdded = !e
if (entity.delete) { if (entity.delete) {
if (!e) return if (!e) return
@ -817,23 +836,21 @@ export class Entities {
if (e === undefined) { if (e === undefined) {
const group = new THREE.Group() as unknown as SceneEntity const group = new THREE.Group() as unknown as SceneEntity
group.originalEntity = entity group.originalEntity = entity
if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block' || entity.name === 'snowball' if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block') {
|| entity.name === 'egg' || entity.name === 'ender_pearl' || entity.name === 'experience_bottle' const item = entity.name === 'tnt'
|| entity.name === 'splash_potion' || entity.name === 'lingering_potion') { ? { name: 'tnt' }
const item = entity.name === 'tnt' || entity.type === 'projectile'
? { name: entity.name }
: entity.name === 'falling_block' : entity.name === 'falling_block'
? { blockState: entity['objectData'] } ? { blockState: entity['objectData'] }
: entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount) : entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
if (item) { if (item) {
const object = this.getItemMesh(item, { const object = this.getItemMesh(item, {
'minecraft:display_context': 'ground', 'minecraft:display_context': 'ground',
}, entity.type === 'projectile') })
if (object) { if (object) {
mesh = object.mesh mesh = object.mesh
if (entity.name === 'item' || entity.type === 'projectile') { if (entity.name === 'item') {
mesh.scale.set(0.5, 0.5, 0.5) mesh.scale.set(0.5, 0.5, 0.5)
mesh.position.set(0, entity.name === 'item' ? 0.2 : 0.1, 0) mesh.position.set(0, 0.2, 0)
} else { } else {
mesh.scale.set(2, 2, 2) mesh.scale.set(2, 2, 2)
mesh.position.set(0, 0.5, 0) mesh.position.set(0, 0.5, 0)
@ -841,8 +858,8 @@ export class Entities {
// set faces // set faces
// mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5) // mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5)
// viewer.scene.add(mesh) // viewer.scene.add(mesh)
const clock = new THREE.Clock()
if (entity.name === 'item') { if (entity.name === 'item') {
const clock = new THREE.Clock()
mesh.onBeforeRender = () => { mesh.onBeforeRender = () => {
const delta = clock.getDelta() const delta = clock.getDelta()
mesh!.rotation.y += delta mesh!.rotation.y += delta
@ -871,9 +888,8 @@ export class Entities {
group.additionalCleanup = () => { group.additionalCleanup = () => {
// important: avoid texture memory leak and gpu slowdown // important: avoid texture memory leak and gpu slowdown
if (object.cleanup) { object.itemsTexture?.dispose()
object.cleanup() object.itemsTextureFlipped?.dispose()
}
} }
} }
} }
@ -884,14 +900,20 @@ export class Entities {
mesh = wrapper mesh = wrapper
if (entity.username) { if (entity.username) {
const nametag = addNametag(entity, { fontFamily: 'mojangles' }, wrapper, this.worldRenderer.version) // todo proper colors
if (nametag) { const nameTag = new NameTagObject(fromFormattedString(entity.username).text, {
nametag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3 font: `48px ${this.entitiesOptions.fontFamily}`,
nametag.scale.multiplyScalar(12) })
} nameTag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
nameTag.renderOrder = 1000
nameTag.name = 'nametag'
//@ts-expect-error
wrapper.add(nameTag)
} }
} else { } else {
mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, { ...overrides, customModel: entity['customModel'] }) mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, overrides)
} }
if (!mesh) return if (!mesh) return
mesh.name = 'mesh' mesh.name = 'mesh'
@ -1147,7 +1169,8 @@ export class Entities {
const cameraPos = this.worldRenderer.cameraObject.position const cameraPos = this.worldRenderer.cameraObject.position
const distance = mesh.position.distanceTo(cameraPos) const distance = mesh.position.distanceTo(cameraPos)
if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) { if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) {
if (this.loadedSkinEntityIds.has(String(entityId))) return if (this.loadedSkinEntityIds.has(entityId)) return
this.loadedSkinEntityIds.add(entityId)
void this.updatePlayerSkin(entityId, mesh.playerObject.realUsername, mesh.playerObject.realPlayerUuid, true, true) void this.updatePlayerSkin(entityId, mesh.playerObject.realUsername, mesh.playerObject.realPlayerUuid, true, true)
} }
} }
@ -1262,9 +1285,8 @@ export class Entities {
const group = new THREE.Object3D() const group = new THREE.Object3D()
group['additionalCleanup'] = () => { group['additionalCleanup'] = () => {
// important: avoid texture memory leak and gpu slowdown // important: avoid texture memory leak and gpu slowdown
if (itemObject.cleanup) { itemObject.itemsTexture?.dispose()
itemObject.cleanup() itemObject.itemsTextureFlipped?.dispose()
}
} }
const itemMesh = itemObject.mesh const itemMesh = itemObject.mesh
group.rotation.z = -Math.PI / 16 group.rotation.z = -Math.PI / 16

View file

@ -44,12 +44,6 @@ const getBackendMethods = (worldRenderer: WorldRendererThree) => {
shakeFromDamage: worldRenderer.cameraShake.shakeFromDamage.bind(worldRenderer.cameraShake), shakeFromDamage: worldRenderer.cameraShake.shakeFromDamage.bind(worldRenderer.cameraShake),
onPageInteraction: worldRenderer.media.onPageInteraction.bind(worldRenderer.media), onPageInteraction: worldRenderer.media.onPageInteraction.bind(worldRenderer.media),
downloadMesherLog: worldRenderer.downloadMesherLog.bind(worldRenderer), downloadMesherLog: worldRenderer.downloadMesherLog.bind(worldRenderer),
addWaypoint: worldRenderer.waypoints.addWaypoint.bind(worldRenderer.waypoints),
removeWaypoint: worldRenderer.waypoints.removeWaypoint.bind(worldRenderer.waypoints),
// New method for updating skybox
setSkyboxImage: worldRenderer.skyboxRenderer.setSkyboxImage.bind(worldRenderer.skyboxRenderer)
} }
} }

View file

@ -357,7 +357,7 @@ export default class HoldingBlock {
'minecraft:display_context': 'firstperson', 'minecraft:display_context': 'firstperson',
'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks, 'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks, 'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
}, false, this.lastItemModelName) }, this.lastItemModelName)
if (result) { if (result) {
const { mesh: itemMesh, isBlock, modelName } = result const { mesh: itemMesh, isBlock, modelName } = result
if (isBlock) { if (isBlock) {

View file

@ -1,427 +0,0 @@
import * as THREE from 'three'
export interface Create3DItemMeshOptions {
depth: number
pixelSize?: number
}
export interface Create3DItemMeshResult {
geometry: THREE.BufferGeometry
totalVertices: number
totalTriangles: number
}
/**
* Creates a 3D item geometry with front/back faces and connecting edges
* from a canvas containing the item texture
*/
export function create3DItemMesh (
canvas: HTMLCanvasElement,
options: Create3DItemMeshOptions
): Create3DItemMeshResult {
const { depth, pixelSize } = options
// Validate canvas dimensions
if (canvas.width <= 0 || canvas.height <= 0) {
throw new Error(`Invalid canvas dimensions: ${canvas.width}x${canvas.height}`)
}
const ctx = canvas.getContext('2d')!
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const { data } = imageData
const w = canvas.width
const h = canvas.height
const halfDepth = depth / 2
const actualPixelSize = pixelSize ?? (1 / Math.max(w, h))
// Find opaque pixels
const isOpaque = (x: number, y: number) => {
if (x < 0 || y < 0 || x >= w || y >= h) return false
const i = (y * w + x) * 4
return data[i + 3] > 128 // alpha > 128
}
const vertices: number[] = []
const indices: number[] = []
const uvs: number[] = []
const normals: number[] = []
let vertexIndex = 0
// Helper to add a vertex
const addVertex = (x: number, y: number, z: number, u: number, v: number, nx: number, ny: number, nz: number) => {
vertices.push(x, y, z)
uvs.push(u, v)
normals.push(nx, ny, nz)
return vertexIndex++
}
// Helper to add a quad (two triangles)
const addQuad = (v0: number, v1: number, v2: number, v3: number) => {
indices.push(v0, v1, v2, v0, v2, v3)
}
// Convert pixel coordinates to world coordinates
const pixelToWorld = (px: number, py: number) => {
const x = (px / w - 0.5) * actualPixelSize * w
const y = -(py / h - 0.5) * actualPixelSize * h
return { x, y }
}
// Create a grid of vertices for front and back faces
const frontVertices: Array<Array<number | null>> = Array.from({ length: h + 1 }, () => Array.from({ length: w + 1 }, () => null))
const backVertices: Array<Array<number | null>> = Array.from({ length: h + 1 }, () => Array.from({ length: w + 1 }, () => null))
// Create vertices at pixel corners
for (let py = 0; py <= h; py++) {
for (let px = 0; px <= w; px++) {
const { x, y } = pixelToWorld(px - 0.5, py - 0.5)
// UV coordinates should map to the texture space of the extracted tile
const u = px / w
const v = py / h
// Check if this vertex is needed for any face or edge
let needVertex = false
// Check all 4 adjacent pixels to see if any are opaque
const adjacentPixels = [
[px - 1, py - 1], // top-left pixel
[px, py - 1], // top-right pixel
[px - 1, py], // bottom-left pixel
[px, py] // bottom-right pixel
]
for (const [adjX, adjY] of adjacentPixels) {
if (isOpaque(adjX, adjY)) {
needVertex = true
break
}
}
if (needVertex) {
frontVertices[py][px] = addVertex(x, y, halfDepth, u, v, 0, 0, 1)
backVertices[py][px] = addVertex(x, y, -halfDepth, u, v, 0, 0, -1)
}
}
}
// Create front and back faces
for (let py = 0; py < h; py++) {
for (let px = 0; px < w; px++) {
if (!isOpaque(px, py)) continue
const v00 = frontVertices[py][px]
const v10 = frontVertices[py][px + 1]
const v11 = frontVertices[py + 1][px + 1]
const v01 = frontVertices[py + 1][px]
const b00 = backVertices[py][px]
const b10 = backVertices[py][px + 1]
const b11 = backVertices[py + 1][px + 1]
const b01 = backVertices[py + 1][px]
if (v00 !== null && v10 !== null && v11 !== null && v01 !== null) {
// Front face
addQuad(v00, v10, v11, v01)
}
if (b00 !== null && b10 !== null && b11 !== null && b01 !== null) {
// Back face (reversed winding)
addQuad(b10, b00, b01, b11)
}
}
}
// Create edge faces for each side of the pixel with proper UVs
for (let py = 0; py < h; py++) {
for (let px = 0; px < w; px++) {
if (!isOpaque(px, py)) continue
const pixelU = (px + 0.5) / w // Center of current pixel
const pixelV = (py + 0.5) / h
// Left edge (x = px)
if (!isOpaque(px - 1, py)) {
const f0 = frontVertices[py][px]
const f1 = frontVertices[py + 1][px]
const b0 = backVertices[py][px]
const b1 = backVertices[py + 1][px]
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
// Create new vertices for edge with current pixel's UV
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, -1, 0, 0)
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, -1, 0, 0)
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, -1, 0, 0)
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, -1, 0, 0)
addQuad(ef0, ef1, eb1, eb0)
}
}
// Right edge (x = px + 1)
if (!isOpaque(px + 1, py)) {
const f0 = frontVertices[py + 1][px + 1]
const f1 = frontVertices[py][px + 1]
const b0 = backVertices[py + 1][px + 1]
const b1 = backVertices[py][px + 1]
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 1, 0, 0)
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 1, 0, 0)
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 1, 0, 0)
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 1, 0, 0)
addQuad(ef0, ef1, eb1, eb0)
}
}
// Top edge (y = py)
if (!isOpaque(px, py - 1)) {
const f0 = frontVertices[py][px]
const f1 = frontVertices[py][px + 1]
const b0 = backVertices[py][px]
const b1 = backVertices[py][px + 1]
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 0, -1, 0)
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 0, -1, 0)
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 0, -1, 0)
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 0, -1, 0)
addQuad(ef0, ef1, eb1, eb0)
}
}
// Bottom edge (y = py + 1)
if (!isOpaque(px, py + 1)) {
const f0 = frontVertices[py + 1][px + 1]
const f1 = frontVertices[py + 1][px]
const b0 = backVertices[py + 1][px + 1]
const b1 = backVertices[py + 1][px]
if (f0 !== null && f1 !== null && b0 !== null && b1 !== null) {
const ef0 = addVertex(vertices[f0 * 3], vertices[f0 * 3 + 1], vertices[f0 * 3 + 2], pixelU, pixelV, 0, 1, 0)
const ef1 = addVertex(vertices[f1 * 3], vertices[f1 * 3 + 1], vertices[f1 * 3 + 2], pixelU, pixelV, 0, 1, 0)
const eb1 = addVertex(vertices[b1 * 3], vertices[b1 * 3 + 1], vertices[b1 * 3 + 2], pixelU, pixelV, 0, 1, 0)
const eb0 = addVertex(vertices[b0 * 3], vertices[b0 * 3 + 1], vertices[b0 * 3 + 2], pixelU, pixelV, 0, 1, 0)
addQuad(ef0, ef1, eb1, eb0)
}
}
}
}
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
geometry.setIndex(indices)
// Compute normals properly
geometry.computeVertexNormals()
return {
geometry,
totalVertices: vertexIndex,
totalTriangles: indices.length / 3
}
}
export interface ItemTextureInfo {
u: number
v: number
sizeX: number
sizeY: number
}
export interface ItemMeshResult {
mesh: THREE.Object3D
itemsTexture?: THREE.Texture
itemsTextureFlipped?: THREE.Texture
cleanup?: () => void
}
/**
* Extracts item texture region to a canvas
*/
export function extractItemTextureToCanvas (
sourceTexture: THREE.Texture,
textureInfo: ItemTextureInfo
): HTMLCanvasElement {
const { u, v, sizeX, sizeY } = textureInfo
// Calculate canvas size - fix the calculation
const canvasWidth = Math.max(1, Math.floor(sizeX * sourceTexture.image.width))
const canvasHeight = Math.max(1, Math.floor(sizeY * sourceTexture.image.height))
const canvas = document.createElement('canvas')
canvas.width = canvasWidth
canvas.height = canvasHeight
const ctx = canvas.getContext('2d')!
ctx.imageSmoothingEnabled = false
// Draw the item texture region to canvas
ctx.drawImage(
sourceTexture.image,
u * sourceTexture.image.width,
v * sourceTexture.image.height,
sizeX * sourceTexture.image.width,
sizeY * sourceTexture.image.height,
0,
0,
canvas.width,
canvas.height
)
return canvas
}
/**
* Creates either a 2D or 3D item mesh based on parameters
*/
export function createItemMesh (
sourceTexture: THREE.Texture,
textureInfo: ItemTextureInfo,
options: {
faceCamera?: boolean
use3D?: boolean
depth?: number
} = {}
): ItemMeshResult {
const { faceCamera = false, use3D = true, depth = 0.04 } = options
const { u, v, sizeX, sizeY } = textureInfo
if (faceCamera) {
// Create sprite for camera-facing items
const itemsTexture = sourceTexture.clone()
itemsTexture.flipY = true
itemsTexture.offset.set(u, 1 - v - sizeY)
itemsTexture.repeat.set(sizeX, sizeY)
itemsTexture.needsUpdate = true
itemsTexture.magFilter = THREE.NearestFilter
itemsTexture.minFilter = THREE.NearestFilter
const spriteMat = new THREE.SpriteMaterial({
map: itemsTexture,
transparent: true,
alphaTest: 0.1,
})
const mesh = new THREE.Sprite(spriteMat)
return {
mesh,
itemsTexture,
cleanup () {
itemsTexture.dispose()
}
}
}
if (use3D) {
// Try to create 3D mesh
try {
const canvas = extractItemTextureToCanvas(sourceTexture, textureInfo)
const { geometry } = create3DItemMesh(canvas, { depth })
// Create texture from canvas for the 3D mesh
const itemsTexture = new THREE.CanvasTexture(canvas)
itemsTexture.magFilter = THREE.NearestFilter
itemsTexture.minFilter = THREE.NearestFilter
itemsTexture.wrapS = itemsTexture.wrapT = THREE.ClampToEdgeWrapping
itemsTexture.flipY = false
itemsTexture.needsUpdate = true
const material = new THREE.MeshStandardMaterial({
map: itemsTexture,
side: THREE.DoubleSide,
transparent: true,
alphaTest: 0.1,
})
const mesh = new THREE.Mesh(geometry, material)
return {
mesh,
itemsTexture,
cleanup () {
itemsTexture.dispose()
geometry.dispose()
if (material.map) material.map.dispose()
material.dispose()
}
}
} catch (error) {
console.warn('Failed to create 3D item mesh, falling back to 2D:', error)
// Fall through to 2D rendering
}
}
// Fallback to 2D flat rendering
const itemsTexture = sourceTexture.clone()
itemsTexture.flipY = true
itemsTexture.offset.set(u, 1 - v - sizeY)
itemsTexture.repeat.set(sizeX, sizeY)
itemsTexture.needsUpdate = true
itemsTexture.magFilter = THREE.NearestFilter
itemsTexture.minFilter = THREE.NearestFilter
const itemsTextureFlipped = itemsTexture.clone()
itemsTextureFlipped.repeat.x *= -1
itemsTextureFlipped.needsUpdate = true
itemsTextureFlipped.offset.set(u + sizeX, 1 - v - sizeY)
const material = new THREE.MeshStandardMaterial({
map: itemsTexture,
transparent: true,
alphaTest: 0.1,
})
const materialFlipped = new THREE.MeshStandardMaterial({
map: itemsTextureFlipped,
transparent: true,
alphaTest: 0.1,
})
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
material, materialFlipped,
])
return {
mesh,
itemsTexture,
itemsTextureFlipped,
cleanup () {
itemsTexture.dispose()
itemsTextureFlipped.dispose()
material.dispose()
materialFlipped.dispose()
}
}
}
/**
* Creates a complete 3D item mesh from a canvas texture
*/
export function createItemMeshFromCanvas (
canvas: HTMLCanvasElement,
options: Create3DItemMeshOptions
): THREE.Mesh {
const { geometry } = create3DItemMesh(canvas, options)
// Base color texture for the item
const colorTexture = new THREE.CanvasTexture(canvas)
colorTexture.magFilter = THREE.NearestFilter
colorTexture.minFilter = THREE.NearestFilter
colorTexture.wrapS = colorTexture.wrapT = THREE.ClampToEdgeWrapping
colorTexture.flipY = false // Important for canvas textures
colorTexture.needsUpdate = true
// Material - no transparency, no alpha test needed for edges
const material = new THREE.MeshBasicMaterial({
map: colorTexture,
side: THREE.DoubleSide,
transparent: true,
alphaTest: 0.1
})
return new THREE.Mesh(geometry, material)
}

View file

@ -10,11 +10,11 @@ export type ResolvedItemModelRender = {
export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: ResourcesManagerCommon, debugIsQuickbar = false, fullBlockModelSupport = false): { export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: ResourcesManagerCommon, debugIsQuickbar = false, fullBlockModelSupport = false): {
texture: string, texture: string,
blockData: Record<string, { slice, path }> & { resolvedModel: BlockModel } | null, blockData?: Record<string, { slice, path }> & { resolvedModel: BlockModel },
scale: number | null, scale?: number,
slice: number[] | null, slice?: number[],
modelName: string | null, modelName?: string,
} => { } | undefined => {
let itemModelName = model.modelName let itemModelName = model.modelName
const isItem = loadedData.itemsByName[itemModelName] const isItem = loadedData.itemsByName[itemModelName]
@ -37,8 +37,6 @@ export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: Res
texture: 'gui', texture: 'gui',
slice: [x, y, atlas.tileSize, atlas.tileSize], slice: [x, y, atlas.tileSize, atlas.tileSize],
scale: 0.25, scale: 0.25,
blockData: null,
modelName: null
} }
} }
} }
@ -65,18 +63,14 @@ export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: Res
return { return {
texture: itemTexture.type, texture: itemTexture.type,
slice: itemTexture.slice, slice: itemTexture.slice,
modelName: itemModelName, modelName: itemModelName
blockData: null,
scale: null
} }
} else { } else {
// is block // is block
return { return {
texture: 'blocks', texture: 'blocks',
blockData: itemTexture, blockData: itemTexture,
modelName: itemModelName, modelName: itemModelName
slice: null,
scale: null
} }
} }
} }

View file

@ -1,406 +0,0 @@
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
// 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) {
await this.setSkyboxImage(this.initialImage)
}
}
async setSkyboxImage (imageUrl: string) {
// Dispose old textures if they exist
if (this.texture) {
this.texture.dispose()
}
// Load the equirectangular texture
const textureLoader = new THREE.TextureLoader()
this.texture = await new Promise((resolve) => {
textureLoader.load(
imageUrl,
(texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping
texture.encoding = THREE.sRGBEncoding
// Keep pixelated look
texture.minFilter = THREE.NearestFilter
texture.magFilter = THREE.NearestFilter
texture.needsUpdate = true
resolve(texture)
}
)
})
// Create or update the skybox
if (this.mesh) {
// Just update the texture on the existing material
this.mesh.material.map = this.texture
this.mesh.material.needsUpdate = true
} else {
// Create a large sphere geometry for the skybox
const geometry = new THREE.SphereGeometry(500, 60, 40)
// Flip the geometry inside out
geometry.scale(-1, 1, 1)
// Create material using the loaded texture
const material = new THREE.MeshBasicMaterial({
map: this.texture,
side: THREE.FrontSide // Changed to FrontSide since we're flipping the geometry
})
// Create and add the skybox mesh
this.mesh = new THREE.Mesh(geometry, material)
this.scene.add(this.mesh)
}
}
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 () {
if (this.texture) {
this.texture.dispose()
}
if (this.mesh) {
this.mesh.geometry.dispose()
;(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

@ -2,7 +2,7 @@ import * as THREE from 'three'
import { WorldRendererThree } from './worldrendererThree' import { WorldRendererThree } from './worldrendererThree'
export interface SoundSystem { export interface SoundSystem {
playSound: (position: { x: number, y: number, z: number }, path: string, volume?: number, pitch?: number, timeout?: number) => void playSound: (position: { x: number, y: number, z: number }, path: string, volume?: number, pitch?: number) => void
destroy: () => void destroy: () => void
} }
@ -10,17 +10,7 @@ export class ThreeJsSound implements SoundSystem {
audioListener: THREE.AudioListener | undefined audioListener: THREE.AudioListener | undefined
private readonly activeSounds = new Set<THREE.PositionalAudio>() private readonly activeSounds = new Set<THREE.PositionalAudio>()
private readonly audioContext: AudioContext | undefined private readonly audioContext: AudioContext | undefined
private readonly soundVolumes = new Map<THREE.PositionalAudio, number>()
baseVolume = 1
constructor (public worldRenderer: WorldRendererThree) { constructor (public worldRenderer: WorldRendererThree) {
worldRenderer.onWorldSwitched.push(() => {
this.stopAll()
})
worldRenderer.onReactiveConfigUpdated('volume', (volume) => {
this.changeVolume(volume)
})
} }
initAudioListener () { initAudioListener () {
@ -29,24 +19,20 @@ export class ThreeJsSound implements SoundSystem {
this.worldRenderer.camera.add(this.audioListener) this.worldRenderer.camera.add(this.audioListener)
} }
playSound (position: { x: number, y: number, z: number }, path: string, volume = 1, pitch = 1, timeout = 500) { playSound (position: { x: number, y: number, z: number }, path: string, volume = 1, pitch = 1) {
this.initAudioListener() this.initAudioListener()
const sound = new THREE.PositionalAudio(this.audioListener!) const sound = new THREE.PositionalAudio(this.audioListener!)
this.activeSounds.add(sound) this.activeSounds.add(sound)
this.soundVolumes.set(sound, volume)
const audioLoader = new THREE.AudioLoader() const audioLoader = new THREE.AudioLoader()
const start = Date.now() const start = Date.now()
void audioLoader.loadAsync(path).then((buffer) => { void audioLoader.loadAsync(path).then((buffer) => {
if (Date.now() - start > timeout) { if (Date.now() - start > 500) return
console.warn('Ignored playing sound', path, 'due to timeout:', timeout, 'ms <', Date.now() - start, 'ms')
return
}
// play // play
sound.setBuffer(buffer) sound.setBuffer(buffer)
sound.setRefDistance(20) sound.setRefDistance(20)
sound.setVolume(volume * this.baseVolume) sound.setVolume(volume)
sound.setPlaybackRate(pitch) // set the pitch sound.setPlaybackRate(pitch) // set the pitch
this.worldRenderer.scene.add(sound) this.worldRenderer.scene.add(sound)
// set sound position // set sound position
@ -57,35 +43,21 @@ export class ThreeJsSound implements SoundSystem {
sound.disconnect() sound.disconnect()
} }
this.activeSounds.delete(sound) this.activeSounds.delete(sound)
this.soundVolumes.delete(sound)
audioLoader.manager.itemEnd(path) audioLoader.manager.itemEnd(path)
} }
sound.play() sound.play()
}) })
} }
stopAll () { destroy () {
// Stop and clean up all active sounds
for (const sound of this.activeSounds) { for (const sound of this.activeSounds) {
if (!sound) continue
sound.stop() sound.stop()
if (sound.source) { if (sound.source) {
sound.disconnect() sound.disconnect()
} }
this.worldRenderer.scene.remove(sound)
} }
this.activeSounds.clear()
this.soundVolumes.clear()
}
changeVolume (volume: number) {
this.baseVolume = volume
for (const [sound, individualVolume] of this.soundVolumes) {
sound.setVolume(individualVolume * this.baseVolume)
}
}
destroy () {
this.stopAll()
// Remove and cleanup audio listener // Remove and cleanup audio listener
if (this.audioListener) { if (this.audioListener) {
this.audioListener.removeFromParent() this.audioListener.removeFromParent()

View file

@ -1,418 +0,0 @@
import * as THREE from 'three'
// Centralized visual configuration (in screen pixels)
export const WAYPOINT_CONFIG = {
// Target size in screen pixels (this controls the final sprite size)
TARGET_SCREEN_PX: 150,
// Canvas size for internal rendering (keep power of 2 for textures)
CANVAS_SIZE: 256,
// Relative positions in canvas (0-1)
LAYOUT: {
DOT_Y: 0.3,
NAME_Y: 0.45,
DISTANCE_Y: 0.55,
},
// Multiplier for canvas internal resolution to keep text crisp
CANVAS_SCALE: 2,
ARROW: {
enabledDefault: false,
pixelSize: 50,
paddingPx: 50,
},
}
export type WaypointSprite = {
group: THREE.Group
sprite: THREE.Sprite
// Offscreen arrow controls
enableOffscreenArrow: (enabled: boolean) => void
setArrowParent: (parent: THREE.Object3D | null) => void
// Convenience combined updater
updateForCamera: (
cameraPosition: THREE.Vector3,
camera: THREE.PerspectiveCamera,
viewportWidthPx: number,
viewportHeightPx: number
) => boolean
// Utilities
setColor: (color: number) => void
setLabel: (label?: string) => void
updateDistanceText: (label: string, distanceText: string) => void
setVisible: (visible: boolean) => void
setPosition: (x: number, y: number, z: number) => void
dispose: () => void
}
export function createWaypointSprite (options: {
position: THREE.Vector3 | { x: number, y: number, z: number },
color?: number,
label?: string,
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
const labelYOffset = options.labelYOffset ?? 1.5
// Build combined sprite
const sprite = createCombinedSprite(color, options.label ?? '', '0m', depthTest)
sprite.renderOrder = 10
let currentLabel = options.label ?? ''
// Offscreen arrow (detached by default)
let arrowSprite: THREE.Sprite | undefined
let arrowParent: THREE.Object3D | null = null
let arrowEnabled = WAYPOINT_CONFIG.ARROW.enabledDefault
// Group for easy add/remove
const group = new THREE.Group()
group.add(sprite)
// Initial position
const { x, y, z } = options.position
group.position.set(x, y, z)
function setColor (newColor: number) {
const canvas = drawCombinedCanvas(newColor, currentLabel, '0m')
const texture = new THREE.CanvasTexture(canvas)
const mat = sprite.material
mat.map?.dispose()
mat.map = texture
mat.needsUpdate = true
}
function setLabel (newLabel?: string) {
currentLabel = newLabel ?? ''
const canvas = drawCombinedCanvas(color, currentLabel, '0m')
const texture = new THREE.CanvasTexture(canvas)
const mat = sprite.material
mat.map?.dispose()
mat.map = texture
mat.needsUpdate = true
}
function updateDistanceText (label: string, distanceText: string) {
const canvas = drawCombinedCanvas(color, label, distanceText)
const texture = new THREE.CanvasTexture(canvas)
const mat = sprite.material
mat.map?.dispose()
mat.map = texture
mat.needsUpdate = true
}
function setVisible (visible: boolean) {
sprite.visible = visible
}
function setPosition (nx: number, ny: number, nz: number) {
group.position.set(nx, ny, nz)
}
// Keep constant pixel size on screen using global config
function updateScaleScreenPixels (
cameraPosition: THREE.Vector3,
cameraFov: number,
distance: number,
viewportHeightPx: number
) {
const vFovRad = cameraFov * Math.PI / 180
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance
// Use configured target screen size
const scale = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.TARGET_SCREEN_PX / viewportHeightPx)
sprite.scale.set(scale, scale, 1)
}
function ensureArrow () {
if (arrowSprite) return
const size = 128
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const ctx = canvas.getContext('2d')!
ctx.clearRect(0, 0, size, size)
// Draw arrow shape
ctx.beginPath()
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()
// Use waypoint color for arrow
const colorHex = `#${color.toString(16).padStart(6, '0')}`
ctx.lineWidth = 6
ctx.strokeStyle = 'black'
ctx.stroke()
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)
arrowSprite.renderOrder = 12
arrowSprite.visible = false
if (arrowParent) arrowParent.add(arrowSprite)
}
function enableOffscreenArrow (enabled: boolean) {
arrowEnabled = enabled
if (!enabled && arrowSprite) arrowSprite.visible = false
}
function setArrowParent (parent: THREE.Object3D | null) {
if (arrowSprite?.parent) arrowSprite.parent.remove(arrowSprite)
arrowParent = parent
if (arrowSprite && parent) parent.add(arrowSprite)
}
function updateOffscreenArrow (
camera: THREE.PerspectiveCamera,
viewportWidthPx: number,
viewportHeightPx: number
): boolean {
if (!arrowEnabled) return true
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
const upWorld = camera.up.clone().normalize()
const right = new THREE.Vector3().copy(forward).cross(upWorld).normalize()
const upCam = new THREE.Vector3().copy(right).cross(forward).normalize()
// Vector from camera to waypoint
const camPos = new THREE.Vector3().setFromMatrixPosition(camera.matrixWorld)
const toWp = new THREE.Vector3(group.position.x, group.position.y, group.position.z).sub(camPos)
// Components in camera basis
const z = toWp.dot(forward)
const x = toWp.dot(right)
const y = toWp.dot(upCam)
const aspect = viewportWidthPx / viewportHeightPx
const vFovRad = camera.fov * Math.PI / 180
const hFovRad = 2 * Math.atan(Math.tan(vFovRad / 2) * aspect)
// Determine if waypoint is inside view frustum using angular checks
const thetaX = Math.atan2(x, z)
const thetaY = Math.atan2(y, z)
const visible = z > 0 && Math.abs(thetaX) <= hFovRad / 2 && Math.abs(thetaY) <= vFovRad / 2
if (visible) {
arrowSprite.visible = false
return true
}
// Direction on screen in normalized frustum units
let rx = thetaX / (hFovRad / 2)
let ry = thetaY / (vFovRad / 2)
// If behind the camera, snap to dominant axis to avoid confusing directions
if (z <= 0) {
if (Math.abs(rx) > Math.abs(ry)) {
rx = Math.sign(rx)
ry = 0
} else {
rx = 0
ry = Math.sign(ry)
}
}
// 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
let ndcY = ry / s
// Apply padding in pixel space by clamping
const padding = WAYPOINT_CONFIG.ARROW.paddingPx
const pxX = ((ndcX + 1) * 0.5) * viewportWidthPx
const pxY = ((1 - ndcY) * 0.5) * viewportHeightPx
const clampedPxX = Math.min(Math.max(pxX, padding), viewportWidthPx - padding)
const clampedPxY = Math.min(Math.max(pxY, padding), viewportHeightPx - padding)
ndcX = (clampedPxX / viewportWidthPx) * 2 - 1
ndcY = -(clampedPxY / viewportHeightPx) * 2 + 1
// Compute world position at a fixed distance in front of the camera using camera basis
const placeDist = Math.max(2, camera.near * 4)
const halfPlaneHeight = Math.tan(vFovRad / 2) * placeDist
const halfPlaneWidth = halfPlaneHeight * aspect
const pos = camPos.clone()
.add(forward.clone().multiplyScalar(placeDist))
.add(right.clone().multiplyScalar(ndcX * halfPlaneWidth))
.add(upCam.clone().multiplyScalar(ndcY * halfPlaneHeight))
// Update arrow sprite
arrowSprite.visible = true
arrowSprite.position.copy(pos)
// Angle for rotation relative to screen right/up (derived from camera up vector)
const angle = Math.atan2(ry, rx)
arrowSprite.material.rotation = angle - Math.PI / 2
// Constant pixel size for arrow (use fixed placement distance)
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * placeDist
const sPx = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.ARROW.pixelSize / viewportHeightPx)
arrowSprite.scale.set(sPx, sPx, 1)
return false
}
function computeDistance (cameraPosition: THREE.Vector3): number {
return cameraPosition.distanceTo(group.position)
}
function updateForCamera (
cameraPosition: THREE.Vector3,
camera: THREE.PerspectiveCamera,
viewportWidthPx: number,
viewportHeightPx: number
): boolean {
const distance = computeDistance(cameraPosition)
// Keep constant pixel size
updateScaleScreenPixels(cameraPosition, camera.fov, distance, viewportHeightPx)
// Update text
updateDistanceText(currentLabel, `${Math.round(distance)}m`)
// Update arrow and visibility
const onScreen = updateOffscreenArrow(camera, viewportWidthPx, viewportHeightPx)
setVisible(onScreen)
return onScreen
}
function dispose () {
const mat = sprite.material
mat.map?.dispose()
mat.dispose()
if (arrowSprite) {
const am = arrowSprite.material
am.map?.dispose()
am.dispose()
}
}
return {
group,
sprite,
enableOffscreenArrow,
setArrowParent,
updateForCamera,
setColor,
setLabel,
updateDistanceText,
setVisible,
setPosition,
dispose,
}
}
// Internal helpers
function drawCombinedCanvas (color: number, id: string, distance: string): HTMLCanvasElement {
const scale = WAYPOINT_CONFIG.CANVAS_SCALE * (globalThis.devicePixelRatio || 1)
const size = WAYPOINT_CONFIG.CANVAS_SIZE * scale
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const ctx = canvas.getContext('2d')!
// Clear canvas
ctx.clearRect(0, 0, size, size)
// Draw dot
const centerX = size / 2
const dotY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DOT_Y)
const radius = Math.round(size * 0.05) // Dot takes up ~12% of canvas height
const borderWidth = Math.max(2, Math.round(4 * scale))
// Outer border (black)
ctx.beginPath()
ctx.arc(centerX, dotY, radius + borderWidth, 0, Math.PI * 2)
ctx.fillStyle = 'black'
ctx.fill()
// Inner circle (colored)
ctx.beginPath()
ctx.arc(centerX, dotY, radius, 0, Math.PI * 2)
ctx.fillStyle = `#${color.toString(16).padStart(6, '0')}`
ctx.fill()
// Text properties
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// Title
const nameFontPx = Math.round(size * 0.08) // ~8% of canvas height
const distanceFontPx = Math.round(size * 0.06) // ~6% of canvas height
ctx.font = `bold ${nameFontPx}px mojangles`
ctx.lineWidth = Math.max(2, Math.round(3 * scale))
const nameY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.NAME_Y)
ctx.strokeStyle = 'black'
ctx.strokeText(id, centerX, nameY)
ctx.fillStyle = 'white'
ctx.fillText(id, centerX, nameY)
// Distance
ctx.font = `bold ${distanceFontPx}px mojangles`
ctx.lineWidth = Math.max(2, Math.round(2 * scale))
const distanceY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DISTANCE_Y)
ctx.strokeStyle = 'black'
ctx.strokeText(distance, centerX, distanceY)
ctx.fillStyle = '#CCCCCC'
ctx.fillText(distance, centerX, distanceY)
return canvas
}
function createCombinedSprite (color: number, id: string, distance: string, depthTest: boolean): THREE.Sprite {
const canvas = drawCombinedCanvas(color, id, distance)
const texture = new THREE.CanvasTexture(canvas)
texture.anisotropy = 1
texture.magFilter = THREE.LinearFilter
texture.minFilter = THREE.LinearFilter
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: 1,
depthTest,
depthWrite: false,
})
const sprite = new THREE.Sprite(material)
sprite.position.set(0, 0, 0)
return sprite
}
export const WaypointHelpers = {
// World-scale constant size helper
computeWorldScale (distance: number, fixedReference = 10) {
return Math.max(0.0001, distance / fixedReference)
},
// Screen-pixel constant size helper
computeScreenPixelScale (
camera: THREE.PerspectiveCamera,
distance: number,
pixelSize: number,
viewportHeightPx: number
) {
const vFovRad = camera.fov * Math.PI / 180
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance
return worldUnitsPerScreenHeightAtDist * (pixelSize / viewportHeightPx)
}
}

View file

@ -1,140 +0,0 @@
import * as THREE from 'three'
import { WorldRendererThree } from './worldrendererThree'
import { createWaypointSprite, type WaypointSprite } from './waypointSprite'
interface Waypoint {
id: string
x: number
y: number
z: number
minDistance: number
color: number
label?: string
sprite: WaypointSprite
}
interface WaypointOptions {
color?: number
label?: string
minDistance?: number
metadata?: any
}
export class WaypointsRenderer {
private readonly waypoints = new Map<string, Waypoint>()
private readonly waypointScene = new THREE.Scene()
constructor (
private readonly worldRenderer: WorldRendererThree
) {
}
private updateWaypoints () {
const playerPos = this.worldRenderer.cameraObject.position
const sizeVec = this.worldRenderer.renderer.getSize(new THREE.Vector2())
for (const waypoint of this.waypoints.values()) {
const waypointPos = new THREE.Vector3(waypoint.x, waypoint.y, waypoint.z)
const distance = playerPos.distanceTo(waypointPos)
const visible = !waypoint.minDistance || distance >= waypoint.minDistance
waypoint.sprite.setVisible(visible)
if (visible) {
// Update position
waypoint.sprite.setPosition(waypoint.x, waypoint.y, waypoint.z)
// Ensure camera-based update each frame
waypoint.sprite.updateForCamera(this.worldRenderer.getCameraPosition(), this.worldRenderer.camera, sizeVec.width, sizeVec.height)
}
}
}
render () {
if (this.waypoints.size === 0) return
// Update waypoint scaling
this.updateWaypoints()
// Render waypoints scene with the world camera
this.worldRenderer.renderer.render(this.waypointScene, this.worldRenderer.camera)
}
// Removed sprite/label texture creation. Use utils/waypointSprite.ts
addWaypoint (
id: string,
x: number,
y: number,
z: number,
options: WaypointOptions = {}
) {
// Remove existing waypoint if it exists
this.removeWaypoint(id)
const color = options.color ?? 0xFF_00_00
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)
this.waypointScene.add(sprite.group)
this.waypoints.set(id, {
id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance,
color, label,
sprite,
})
}
removeWaypoint (id: string) {
const waypoint = this.waypoints.get(id)
if (waypoint) {
this.waypointScene.remove(waypoint.sprite.group)
waypoint.sprite.dispose()
this.waypoints.delete(id)
}
}
clear () {
for (const id of this.waypoints.keys()) {
this.removeWaypoint(id)
}
}
testWaypoint () {
this.addWaypoint('Test Point', 0, 70, 0, { color: 0x00_FF_00, label: 'Test Point' })
this.addWaypoint('Spawn', 0, 64, 0, { color: 0xFF_FF_00, label: 'Spawn' })
this.addWaypoint('Far Point', 100, 70, 100, { color: 0x00_00_FF, label: 'Far Point' })
}
getWaypoint (id: string): Waypoint | undefined {
return this.waypoints.get(id)
}
getAllWaypoints (): Waypoint[] {
return [...this.waypoints.values()]
}
setWaypointColor (id: string, color: number) {
const waypoint = this.waypoints.get(id)
if (waypoint) {
waypoint.sprite.setColor(color)
waypoint.color = color
}
}
setWaypointLabel (id: string, label?: string) {
const waypoint = this.waypoints.get(id)
if (waypoint) {
waypoint.label = label
waypoint.sprite.setLabel(label)
}
}
}

View file

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

View file

@ -3,7 +3,6 @@ import { Vec3 } from 'vec3'
import nbt from 'prismarine-nbt' import nbt from 'prismarine-nbt'
import PrismarineChatLoader from 'prismarine-chat' import PrismarineChatLoader from 'prismarine-chat'
import * as tweenJs from '@tweenjs/tween.js' import * as tweenJs from '@tweenjs/tween.js'
import { Biome } from 'minecraft-data'
import { renderSign } from '../sign-renderer' import { renderSign } from '../sign-renderer'
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer' import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
import { chunkPos, sectionPos } from '../lib/simpleUtils' import { chunkPos, sectionPos } from '../lib/simpleUtils'
@ -24,14 +23,12 @@ import { ThreeJsSound } from './threeJsSound'
import { CameraShake } from './cameraShake' import { CameraShake } from './cameraShake'
import { ThreeJsMedia } from './threeJsMedia' import { ThreeJsMedia } from './threeJsMedia'
import { Fountain } from './threeJsParticles' import { Fountain } from './threeJsParticles'
import { WaypointsRenderer } from './waypoints'
import { DEFAULT_TEMPERATURE, SkyboxRenderer } from './skyboxRenderer'
type SectionKey = string type SectionKey = string
export class WorldRendererThree extends WorldRendererCommon { export class WorldRendererThree extends WorldRendererCommon {
outputFormat = 'threeJs' as const outputFormat = 'threeJs' as const
sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean }> = {} sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean, hasSkylight?: boolean }> = {}
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>() chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
signsCache = new Map<string, any>() signsCache = new Map<string, any>()
starField: StarField starField: StarField
@ -51,7 +48,6 @@ export class WorldRendererThree extends WorldRendererCommon {
cameraContainer: THREE.Object3D cameraContainer: THREE.Object3D
media: ThreeJsMedia media: ThreeJsMedia
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] } waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
waypoints: WaypointsRenderer
camera: THREE.PerspectiveCamera camera: THREE.PerspectiveCamera
renderTimeAvg = 0 renderTimeAvg = 0
sectionsOffsetsAnimations = {} as { sectionsOffsetsAnimations = {} as {
@ -73,7 +69,6 @@ export class WorldRendererThree extends WorldRendererCommon {
} }
fountains: Fountain[] = [] fountains: Fountain[] = []
DEBUG_RAYCAST = false DEBUG_RAYCAST = false
skyboxRenderer: SkyboxRenderer
private currentPosTween?: tweenJs.Tween<THREE.Vector3> private currentPosTween?: tweenJs.Tween<THREE.Vector3>
private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }> private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
@ -97,10 +92,6 @@ export class WorldRendererThree extends WorldRendererCommon {
this.holdingBlock = new HoldingBlock(this) this.holdingBlock = new HoldingBlock(this)
this.holdingBlockLeft = new HoldingBlock(this, true) this.holdingBlockLeft = new HoldingBlock(this, true)
// Initialize skybox renderer
this.skyboxRenderer = new SkyboxRenderer(this.scene, this.worldRendererConfig.defaultSkybox, null)
void this.skyboxRenderer.init()
this.addDebugOverlay() this.addDebugOverlay()
this.resetScene() this.resetScene()
void this.init() void this.init()
@ -108,8 +99,6 @@ export class WorldRendererThree extends WorldRendererCommon {
this.soundSystem = new ThreeJsSound(this) this.soundSystem = new ThreeJsSound(this)
this.cameraShake = new CameraShake(this, this.onRender) this.cameraShake = new CameraShake(this, this.onRender)
this.media = new ThreeJsMedia(this) this.media = new ThreeJsMedia(this)
this.waypoints = new WaypointsRenderer(this)
// this.fountain = new Fountain(this.scene, this.scene, { // this.fountain = new Fountain(this.scene, this.scene, {
// position: new THREE.Vector3(0, 10, 0), // position: new THREE.Vector3(0, 10, 0),
// }) // })
@ -130,8 +119,6 @@ export class WorldRendererThree extends WorldRendererCommon {
this.protocolCustomBlocks.clear() this.protocolCustomBlocks.clear()
// Reset section animations // Reset section animations
this.sectionsOffsetsAnimations = {} this.sectionsOffsetsAnimations = {}
// Clear waypoints
this.waypoints.clear()
}) })
} }
@ -174,18 +161,23 @@ export class WorldRendererThree extends WorldRendererCommon {
override watchReactivePlayerState () { override watchReactivePlayerState () {
super.watchReactivePlayerState() super.watchReactivePlayerState()
this.onReactivePlayerStateUpdated('inWater', (value) => { this.onReactivePlayerStateUpdated('inWater', (value) => {
this.skyboxRenderer.updateWaterState(value, this.playerStateReactive.waterBreathing) this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null
})
this.onReactivePlayerStateUpdated('waterBreathing', (value) => {
this.skyboxRenderer.updateWaterState(this.playerStateReactive.inWater, value)
}) })
this.onReactivePlayerStateUpdated('ambientLight', (value) => { this.onReactivePlayerStateUpdated('ambientLight', (value) => {
if (!value) return if (!value) return
this.ambientLight.intensity = value if (this.worldRendererConfig.legacyLighting) {
this.ambientLight.intensity = value
} else {
this.ambientLight.intensity = 1
}
}) })
this.onReactivePlayerStateUpdated('directionalLight', (value) => { this.onReactivePlayerStateUpdated('directionalLight', (value) => {
if (!value) return if (!value) return
this.directionalLight.intensity = value if (this.worldRendererConfig.legacyLighting) {
this.directionalLight.intensity = value
} else {
this.directionalLight.intensity = 0.4
}
}) })
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => { this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes) this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
@ -206,9 +198,6 @@ export class WorldRendererThree extends WorldRendererCommon {
this.onReactiveConfigUpdated('showChunkBorders', (value) => { this.onReactiveConfigUpdated('showChunkBorders', (value) => {
this.updateShowChunksBorder(value) this.updateShowChunksBorder(value)
}) })
this.onReactiveConfigUpdated('defaultSkybox', (value) => {
this.skyboxRenderer.updateDefaultSkybox(value)
})
} }
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) { changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
@ -271,25 +260,40 @@ export class WorldRendererThree extends WorldRendererCommon {
} else { } else {
this.starField.remove() this.starField.remove()
} }
this.skyboxRenderer.updateTime(newTime)
} }
biomeUpdated (biome: Biome): void { skylightUpdated (): void {
if (biome?.temperature !== undefined) { let updated = 0
this.skyboxRenderer.updateTemperature(biome.temperature) for (const sectionKey of Object.keys(this.sectionObjects)) {
if (this.sectionObjects[sectionKey].hasSkylight) {
// set section to be updated
const [x, y, z] = sectionKey.split(',').map(Number)
this.setSectionDirty(new Vec3(x, y, z))
updated++
}
} }
}
biomeReset (): void { console.log(`Skylight changed to ${this.skyLight}. Updated`, updated, 'sections')
// Reset to default temperature when biome is unknown
this.skyboxRenderer.updateTemperature(DEFAULT_TEMPERATURE)
} }
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) { getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
return getItemUv(item, specificProps, this.resourcesManager, this.playerStateReactive) return getItemUv(item, specificProps, this.resourcesManager, this.playerStateReactive)
} }
debugOnlySunlightSections (enable: boolean, state = true) {
for (const sectionKey of Object.keys(this.sectionObjects)) {
if (!enable) {
this.sectionObjects[sectionKey].visible = true
continue
}
if (this.sectionObjects[sectionKey].hasSkylight) {
this.sectionObjects[sectionKey].visible = state
} else {
this.sectionObjects[sectionKey].visible = false
}
}
}
async demoModel () { async demoModel () {
//@ts-expect-error //@ts-expect-error
const pos = cursorBlockRel(0, 1, 0).position const pos = cursorBlockRel(0, 1, 0).position
@ -377,7 +381,7 @@ export class WorldRendererThree extends WorldRendererCommon {
// debugRecomputedDeletedObjects = 0 // debugRecomputedDeletedObjects = 0
handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void { handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void {
if (data.type !== 'geometry') return if (data.type !== 'geometry') return
let object: THREE.Object3D = this.sectionObjects[data.key] let object = this.sectionObjects[data.key]
if (object) { if (object) {
this.scene.remove(object) this.scene.remove(object)
disposeObject(object) disposeObject(object)
@ -436,7 +440,10 @@ export class WorldRendererThree extends WorldRendererCommon {
object.add(head) object.add(head)
} }
} }
object.hasSkylight = data.geometry.hasSkylight
this.sectionObjects[data.key] = object this.sectionObjects[data.key] = object
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) { if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
object.visible = false object.visible = false
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}` const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
@ -485,7 +492,7 @@ export class WorldRendererThree extends WorldRendererCommon {
return worldPos return worldPos
} }
getSectionCameraPosition () { getWorldCameraPosition () {
const pos = this.getCameraPosition() const pos = this.getCameraPosition()
return new Vec3( return new Vec3(
Math.floor(pos.x / 16), Math.floor(pos.x / 16),
@ -495,7 +502,7 @@ export class WorldRendererThree extends WorldRendererCommon {
} }
updateCameraSectionPos () { updateCameraSectionPos () {
const newSectionPos = this.getSectionCameraPosition() const newSectionPos = this.getWorldCameraPosition()
if (!this.cameraSectionPos.equals(newSectionPos)) { if (!this.cameraSectionPos.equals(newSectionPos)) {
this.cameraSectionPos = newSectionPos this.cameraSectionPos = newSectionPos
this.cameraSectionPositionUpdate() this.cameraSectionPositionUpdate()
@ -734,10 +741,6 @@ export class WorldRendererThree extends WorldRendererCommon {
this.cursorBlock.render() this.cursorBlock.render()
this.updateSectionOffsets() this.updateSectionOffsets()
// Update skybox position to follow camera
const cameraPos = this.getCameraPosition()
this.skyboxRenderer.update(cameraPos, this.viewDistance)
const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov
if (sizeOrFovChanged) { if (sizeOrFovChanged) {
const size = this.renderer.getSize(new THREE.Vector2()) const size = this.renderer.getSize(new THREE.Vector2())
@ -773,8 +776,6 @@ export class WorldRendererThree extends WorldRendererCommon {
fountain.render() fountain.render()
} }
this.waypoints.render()
for (const onRender of this.onRender) { for (const onRender of this.onRender) {
onRender() onRender()
} }
@ -787,17 +788,12 @@ export class WorldRendererThree extends WorldRendererCommon {
} }
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) { renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
let textureData: string const textures = blockEntity.SkullOwner?.Properties?.textures[0]
if (blockEntity.SkullOwner) { if (!textures) return
textureData = blockEntity.SkullOwner.Properties?.textures?.[0]?.Value
} else {
textureData = blockEntity.profile?.properties?.find(p => p.name === 'textures')?.value
}
if (!textureData) return
try { try {
const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString()) const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
let skinUrl = decodedData.textures?.SKIN?.url let skinUrl = textureData.textures?.SKIN?.url
const { skinTexturesProxy } = this.worldRendererConfig const { skinTexturesProxy } = this.worldRendererConfig
if (skinTexturesProxy) { if (skinTexturesProxy) {
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy) skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
@ -982,7 +978,6 @@ export class WorldRendererThree extends WorldRendererCommon {
destroy (): void { destroy (): void {
super.destroy() super.destroy()
this.skyboxRenderer.dispose()
} }
shouldObjectVisible (object: THREE.Object3D) { shouldObjectVisible (object: THREE.Object3D) {
@ -1066,13 +1061,6 @@ class StarField {
constructor ( constructor (
private readonly worldRenderer: WorldRendererThree private readonly worldRenderer: WorldRendererThree
) { ) {
const clock = new THREE.Clock()
const speed = 0.2
this.worldRenderer.onRender.push(() => {
if (!this.points) return
this.points.position.copy(this.worldRenderer.getCameraPosition());
(this.points.material as StarfieldMaterial).uniforms.time.value = clock.getElapsedTime() * speed
})
} }
addToScene () { addToScene () {
@ -1083,6 +1071,7 @@ class StarField {
const count = 7000 const count = 7000
const factor = 7 const factor = 7
const saturation = 10 const saturation = 10
const speed = 0.2
const geometry = new THREE.BufferGeometry() const geometry = new THREE.BufferGeometry()
@ -1115,6 +1104,11 @@ class StarField {
this.points = new THREE.Points(geometry, material) this.points = new THREE.Points(geometry, material)
this.worldRenderer.scene.add(this.points) this.worldRenderer.scene.add(this.points)
const clock = new THREE.Clock()
this.points.onBeforeRender = (renderer, scene, camera) => {
this.points?.position.copy?.(this.worldRenderer.getCameraPosition())
material.uniforms.time.value = clock.getElapsedTime() * speed
}
this.points.renderOrder = -1 this.points.renderOrder = -1
} }

View file

@ -139,13 +139,6 @@ const appConfig = defineConfig({
// 50kb limit for data uri // 50kb limit for data uri
dataUriLimit: SINGLE_FILE_BUILD ? 1 * 1024 * 1024 * 1024 : 50 * 1024 dataUriLimit: SINGLE_FILE_BUILD ? 1 * 1024 * 1024 * 1024 : 50 * 1024
}, },
performance: {
// prefetch: {
// include(filename) {
// return filename.includes('mc-data') || filename.includes('mc-assets')
// },
// },
},
source: { source: {
entry: { entry: {
index: './src/index.ts', index: './src/index.ts',
@ -161,7 +154,7 @@ const appConfig = defineConfig({
'process.platform': '"browser"', 'process.platform': '"browser"',
'process.env.GITHUB_URL': 'process.env.GITHUB_URL':
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}` || githubRepositoryFallback}`), JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}` || githubRepositoryFallback}`),
'process.env.ALWAYS_MINIMAL_SERVER_UI': JSON.stringify(process.env.ALWAYS_MINIMAL_SERVER_UI), 'process.env.DEPS_VERSIONS': JSON.stringify({}),
'process.env.RELEASE_TAG': JSON.stringify(releaseTag), 'process.env.RELEASE_TAG': JSON.stringify(releaseTag),
'process.env.RELEASE_LINK': JSON.stringify(releaseLink), 'process.env.RELEASE_LINK': JSON.stringify(releaseLink),
'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog), 'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog),
@ -197,7 +190,7 @@ const appConfig = defineConfig({
childProcess.execSync('tsx ./scripts/optimizeBlockCollisions.ts', { stdio: 'inherit' }) childProcess.execSync('tsx ./scripts/optimizeBlockCollisions.ts', { stdio: 'inherit' })
} }
// childProcess.execSync(['tsx', './scripts/genLargeDataAliases.ts', ...(SINGLE_FILE_BUILD ? ['--compressed'] : [])].join(' '), { stdio: 'inherit' }) // childProcess.execSync(['tsx', './scripts/genLargeDataAliases.ts', ...(SINGLE_FILE_BUILD ? ['--compressed'] : [])].join(' '), { stdio: 'inherit' })
genLargeDataAliases(SINGLE_FILE_BUILD || process.env.ALWAYS_COMPRESS_LARGE_DATA === 'true') genLargeDataAliases(SINGLE_FILE_BUILD)
fsExtra.copySync('./node_modules/mc-assets/dist/other-textures/latest/entity', './dist/textures/entity') fsExtra.copySync('./node_modules/mc-assets/dist/other-textures/latest/entity', './dist/textures/entity')
fsExtra.copySync('./assets/background', './dist/background') fsExtra.copySync('./assets/background', './dist/background')
fs.copyFileSync('./assets/favicon.png', './dist/favicon.png') fs.copyFileSync('./assets/favicon.png', './dist/favicon.png')
@ -240,10 +233,6 @@ const appConfig = defineConfig({
prep() prep()
}) })
build.onAfterBuild(async () => { build.onAfterBuild(async () => {
if (fs.readdirSync('./assets/customTextures').length > 0) {
childProcess.execSync('tsx ./scripts/patchAssets.ts', { stdio: 'inherit' })
}
if (SINGLE_FILE_BUILD) { if (SINGLE_FILE_BUILD) {
// check that only index.html is in the dist/single folder // check that only index.html is in the dist/single folder
const singleBuildFiles = fs.readdirSync('./dist/single') const singleBuildFiles = fs.readdirSync('./dist/single')

View file

@ -16,8 +16,7 @@ export const genLargeDataAliases = async (isCompressed: boolean) => {
let str = `${decoderCode}\nexport const importLargeData = async (mod: ${Object.keys(modules).map(x => `'${x}'`).join(' | ')}) => {\n` let str = `${decoderCode}\nexport const importLargeData = async (mod: ${Object.keys(modules).map(x => `'${x}'`).join(' | ')}) => {\n`
for (const [module, { compressed, raw }] of Object.entries(modules)) { for (const [module, { compressed, raw }] of Object.entries(modules)) {
const chunkName = module === 'mcData' ? 'mc-data' : 'mc-assets'; let importCode = `(await import('${isCompressed ? compressed : raw}')).default`;
let importCode = `(await import(/* webpackChunkName: "${chunkName}" */ '${isCompressed ? compressed : raw}')).default`;
if (isCompressed) { if (isCompressed) {
importCode = `JSON.parse(decompressFromBase64(${importCode}))` importCode = `JSON.parse(decompressFromBase64(${importCode}))`
} }
@ -31,8 +30,6 @@ export const genLargeDataAliases = async (isCompressed: boolean) => {
const decoderCode = /* ts */ ` const decoderCode = /* ts */ `
import pako from 'pako'; import pako from 'pako';
globalThis.pako = { inflate: pako.inflate.bind(pako) }
function decompressFromBase64(input) { function decompressFromBase64(input) {
console.time('decompressFromBase64') console.time('decompressFromBase64')
// Decode the Base64 string // Decode the Base64 string

View file

@ -6,8 +6,8 @@ import { dirname } from 'node:path'
import supportedVersions from '../src/supportedVersions.mjs' import supportedVersions from '../src/supportedVersions.mjs'
import { gzipSizeFromFileSync } from 'gzip-size' import { gzipSizeFromFileSync } from 'gzip-size'
import fs from 'fs' import fs from 'fs'
import { default as _JsonOptimizer } from '../src/optimizeJson' import {default as _JsonOptimizer} from '../src/optimizeJson'
import { gzipSync } from 'zlib' import { gzipSync } from 'zlib';
import MinecraftData from 'minecraft-data' import MinecraftData from 'minecraft-data'
import MCProtocol from 'minecraft-protocol' import MCProtocol from 'minecraft-protocol'
@ -21,12 +21,12 @@ const require = Module.createRequire(import.meta.url)
const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json') const dataPaths = require('minecraft-data/minecraft-data/data/dataPaths.json')
function toMajor(version) { function toMajor (version) {
const [a, b] = (version + '').split('.') const [a, b] = (version + '').split('.')
return `${a}.${b}` return `${a}.${b}`
} }
let versions = {} const versions = {}
const dataTypes = new Set() const dataTypes = new Set()
for (const [version, dataSet] of Object.entries(dataPaths.pc)) { for (const [version, dataSet] of Object.entries(dataPaths.pc)) {
@ -42,31 +42,6 @@ const versionToNumber = (ver) => {
return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}` return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}`
} }
// Version clipping support
const minVersion = process.env.MIN_MC_VERSION
const maxVersion = process.env.MAX_MC_VERSION
// Filter versions based on MIN_VERSION and MAX_VERSION if provided
if (minVersion || maxVersion) {
const filteredVersions = {}
const minVersionNum = minVersion ? versionToNumber(minVersion) : 0
const maxVersionNum = maxVersion ? versionToNumber(maxVersion) : Infinity
for (const [version, dataSet] of Object.entries(versions)) {
const versionNum = versionToNumber(version)
if (versionNum >= minVersionNum && versionNum <= maxVersionNum) {
filteredVersions[version] = dataSet
}
}
versions = filteredVersions
console.log(`Version clipping applied: ${minVersion || 'none'} to ${maxVersion || 'none'}`)
console.log(`Processing ${Object.keys(versions).length} versions:`, Object.keys(versions).sort((a, b) => versionToNumber(a) - versionToNumber(b)))
}
console.log('Bundling version range:', Object.keys(versions)[0], 'to', Object.keys(versions).at(-1))
// if not included here (even as {}) will not be bundled & accessible! // if not included here (even as {}) will not be bundled & accessible!
// const compressedOutput = !!process.env.SINGLE_FILE_BUILD // const compressedOutput = !!process.env.SINGLE_FILE_BUILD
const compressedOutput = true const compressedOutput = true
@ -82,27 +57,22 @@ const dataTypeBundling2 = {
} }
} }
const dataTypeBundling = { const dataTypeBundling = {
language: process.env.SKIP_MC_DATA_LANGUAGE === 'true' ? { language: {
raw: {}
} : {
ignoreRemoved: true, ignoreRemoved: true,
ignoreChanges: true ignoreChanges: true
}, },
blocks: { blocks: {
arrKey: 'name', arrKey: 'name',
processData(current, prev, _, version) { processData (current, prev) {
for (const block of current) { for (const block of current) {
const prevBlock = prev?.find(x => x.name === block.name)
if (block.transparent) { if (block.transparent) {
const forceOpaque = block.name.includes('shulker_box') || block.name.match(/^double_.+_slab\d?$/) || ['melon_block', 'lit_pumpkin', 'lit_redstone_ore', 'lit_furnace'].includes(block.name) const forceOpaque = block.name.includes('shulker_box') || block.name.match(/^double_.+_slab\d?$/) || ['melon_block', 'lit_pumpkin', 'lit_redstone_ore', 'lit_furnace'].includes(block.name)
const prevBlock = prev?.find(x => x.name === block.name);
if (forceOpaque || (prevBlock && !prevBlock.transparent)) { if (forceOpaque || (prevBlock && !prevBlock.transparent)) {
block.transparent = false block.transparent = false
} }
} }
if (block.hardness === 0 && prevBlock && prevBlock.hardness > 0) {
block.hardness = prevBlock.hardness
}
} }
} }
// ignoreRemoved: true, // ignoreRemoved: true,
@ -166,9 +136,7 @@ const dataTypeBundling = {
blockLoot: { blockLoot: {
arrKey: 'block' arrKey: 'block'
}, },
recipes: process.env.SKIP_MC_DATA_RECIPES === 'true' ? { recipes: {
raw: {}
} : {
raw: true raw: true
// processData: processRecipes // processData: processRecipes
}, },
@ -182,7 +150,7 @@ const dataTypeBundling = {
// } // }
} }
function processRecipes(current, prev, getData, version) { function processRecipes (current, prev, getData, version) {
// can require the same multiple times per different versions // can require the same multiple times per different versions
if (current._proccessed) return if (current._proccessed) return
const items = getData('items') const items = getData('items')
@ -274,39 +242,30 @@ for (const [i, [version, dataSet]] of versionsArr.reverse().entries()) {
for (const [dataType, dataPath] of Object.entries(dataSet)) { for (const [dataType, dataPath] of Object.entries(dataSet)) {
const config = dataTypeBundling[dataType] const config = dataTypeBundling[dataType]
if (!config) continue if (!config) continue
const ignoreCollisionShapes = dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13') if (dataType === 'blockCollisionShapes' && versionToNumber(version) >= versionToNumber('1.13')) {
// contents += ` get ${dataType} () { return window.globalGetCollisionShapes?.("${version}") },\n`
continue
}
let injectCode = '' let injectCode = ''
const getRealData = (type) => { const getData = (type) => {
const loc = `minecraft-data/data/${dataSet[type]}/` const loc = `minecraft-data/data/${dataSet[type]}/`
const dataPathAbsolute = require.resolve(`minecraft-data/${loc}${type}`) const dataPathAbsolute = require.resolve(`minecraft-data/${loc}${type}`)
// const data = fs.readFileSync(dataPathAbsolute, 'utf8') // const data = fs.readFileSync(dataPathAbsolute, 'utf8')
const dataRaw = require(dataPathAbsolute) const dataRaw = require(dataPathAbsolute)
return dataRaw return dataRaw
} }
const dataRaw = getRealData(dataType) const dataRaw = getData(dataType)
let rawData = dataRaw let rawData = dataRaw
if (config.raw) { if (config.raw) {
rawDataVersions[dataType] ??= {} rawDataVersions[dataType] ??= {}
rawDataVersions[dataType][version] = rawData rawDataVersions[dataType][version] = rawData
if (config.raw === true) { rawData = dataRaw
rawData = dataRaw
} else {
rawData = config.raw
}
if (ignoreCollisionShapes && dataType === 'blockCollisionShapes') {
rawData = {
blocks: {},
shapes: {}
}
}
} else { } else {
if (!diffSources[dataType]) { if (!diffSources[dataType]) {
diffSources[dataType] = new JsonOptimizer(config.arrKey, config.ignoreChanges, config.ignoreRemoved) diffSources[dataType] = new JsonOptimizer(config.arrKey, config.ignoreChanges, config.ignoreRemoved)
} }
try { try {
config.processData?.(dataRaw, previousData[dataType], getRealData, version) config.processData?.(dataRaw, previousData[dataType], getData, version)
diffSources[dataType].recordDiff(version, dataRaw) diffSources[dataType].recordDiff(version, dataRaw)
injectCode = `restoreDiff(sources, ${JSON.stringify(dataType)}, ${JSON.stringify(version)})` injectCode = `restoreDiff(sources, ${JSON.stringify(dataType)}, ${JSON.stringify(version)})`
} catch (err) { } catch (err) {
@ -338,16 +297,16 @@ console.log('total size (mb)', totalSize / 1024 / 1024)
console.log( console.log(
'size per data type (mb, %)', 'size per data type (mb, %)',
Object.fromEntries(Object.entries(sizePerDataType).map(([dataType, size]) => { Object.fromEntries(Object.entries(sizePerDataType).map(([dataType, size]) => {
return [dataType, [size / 1024 / 1024, Math.round(size / totalSize * 100)]] return [dataType, [size / 1024 / 1024, Math.round(size / totalSize * 100)]];
}).sort((a, b) => { }).sort((a, b) => {
//@ts-ignore //@ts-ignore
return b[1][1] - a[1][1] return b[1][1] - a[1][1];
})) }))
) )
function compressToBase64(input) { function compressToBase64(input) {
const buffer = gzipSync(input) const buffer = gzipSync(input);
return buffer.toString('base64') return buffer.toString('base64');
} }
const filePath = './generated/minecraft-data-optimized.json' const filePath = './generated/minecraft-data-optimized.json'
@ -371,7 +330,6 @@ console.log('size', fs.lstatSync(filePath).size / 1000 / 1000, gzipSizeFromFileS
const { defaultVersion } = MCProtocol const { defaultVersion } = MCProtocol
const data = MinecraftData(defaultVersion) const data = MinecraftData(defaultVersion)
console.log('defaultVersion', defaultVersion, !!data)
const initialMcData = { const initialMcData = {
[defaultVersion]: { [defaultVersion]: {
version: data.version, version: data.version,

View file

@ -1,137 +0,0 @@
import blocksAtlas from 'mc-assets/dist/blocksAtlases.json'
import itemsAtlas from 'mc-assets/dist/itemsAtlases.json'
import * as fs from 'fs'
import * as path from 'path'
import sharp from 'sharp'
interface AtlasFile {
latest: {
suSv: number
tileSize: number
width: number
height: number
textures: {
[key: string]: {
u: number
v: number
su: number
sv: number
tileIndex: number
}
}
}
}
async function patchTextureAtlas(
atlasType: 'blocks' | 'items',
atlasData: AtlasFile,
customTexturesDir: string,
distDir: string
) {
// Check if custom textures directory exists and has files
if (!fs.existsSync(customTexturesDir) || fs.readdirSync(customTexturesDir).length === 0) {
return
}
// Find the latest atlas file
const atlasFiles = fs.readdirSync(distDir)
.filter(file => file.startsWith(`${atlasType}AtlasLatest`) && file.endsWith('.png'))
.sort()
if (atlasFiles.length === 0) {
console.log(`No ${atlasType}AtlasLatest.png found in ${distDir}`)
return
}
const latestAtlasFile = atlasFiles[atlasFiles.length - 1]
const atlasPath = path.join(distDir, latestAtlasFile)
console.log(`Patching ${atlasPath}`)
// Get atlas dimensions
const atlasMetadata = await sharp(atlasPath).metadata()
if (!atlasMetadata.width || !atlasMetadata.height) {
throw new Error(`Failed to get atlas dimensions for ${atlasPath}`)
}
// Process each custom texture
const customTextureFiles = fs.readdirSync(customTexturesDir)
.filter(file => file.endsWith('.png'))
if (customTextureFiles.length === 0) return
// Prepare composite operations
const composites: sharp.OverlayOptions[] = []
for (const textureFile of customTextureFiles) {
const textureName = path.basename(textureFile, '.png')
if (atlasData.latest.textures[textureName]) {
const textureData = atlasData.latest.textures[textureName]
const customTexturePath = path.join(customTexturesDir, textureFile)
try {
// Convert UV coordinates to pixel coordinates
const x = Math.round(textureData.u * atlasMetadata.width)
const y = Math.round(textureData.v * atlasMetadata.height)
const width = Math.round((textureData.su ?? atlasData.latest.suSv) * atlasMetadata.width)
const height = Math.round((textureData.sv ?? atlasData.latest.suSv) * atlasMetadata.height)
// Resize custom texture to match atlas dimensions and add to composite operations
const resizedTextureBuffer = await sharp(customTexturePath)
.resize(width, height, {
fit: 'fill',
kernel: 'nearest' // Preserve pixel art quality
})
.png()
.toBuffer()
composites.push({
input: resizedTextureBuffer,
left: x,
top: y,
blend: 'over'
})
console.log(`Prepared ${textureName} at (${x}, ${y}) with size (${width}, ${height})`)
} catch (error) {
console.error(`Failed to prepare ${textureName}:`, error)
}
} else {
console.warn(`Texture ${textureName} not found in ${atlasType} atlas`)
}
}
if (composites.length > 0) {
// Apply all patches at once using Sharp's composite
await sharp(atlasPath)
.composite(composites)
.png()
.toFile(atlasPath + '.tmp')
// Replace original with patched version
fs.renameSync(atlasPath + '.tmp', atlasPath)
console.log(`Saved patched ${atlasType} atlas to ${atlasPath}`)
}
}
async function main() {
const customBlocksDir = './assets/customTextures/blocks'
const customItemsDir = './assets/customTextures/items'
const distDir = './dist/static/image'
try {
// Patch blocks atlas
await patchTextureAtlas('blocks', blocksAtlas as unknown as AtlasFile, customBlocksDir, distDir)
// Patch items atlas
await patchTextureAtlas('items', itemsAtlas as unknown as AtlasFile, customItemsDir, distDir)
console.log('Texture atlas patching completed!')
} catch (error) {
console.error('Failed to patch texture atlases:', error)
process.exit(1)
}
}
// Run the script
main()

View file

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

View file

@ -12,7 +12,6 @@ export type AppQsParams = {
username?: string username?: string
lockConnect?: string lockConnect?: string
autoConnect?: string autoConnect?: string
alwaysReconnect?: string
// googledrive.ts params // googledrive.ts params
state?: string state?: string
// ServersListProvider.tsx params // ServersListProvider.tsx params
@ -47,7 +46,6 @@ export type AppQsParams = {
connectText?: string connectText?: string
freezeSettings?: string freezeSettings?: string
testIosCrash?: string testIosCrash?: string
addPing?: string
// Replay params // Replay params
replayFilter?: string replayFilter?: string

View file

@ -17,8 +17,6 @@ import { options } from './optionsStorage'
import { ResourcesManager, ResourcesManagerTransferred } from './resourcesManager' import { ResourcesManager, ResourcesManagerTransferred } from './resourcesManager'
import { watchOptionsAfterWorldViewInit } from './watchOptions' import { watchOptionsAfterWorldViewInit } from './watchOptions'
import { loadMinecraftData } from './connect' import { loadMinecraftData } from './connect'
import { reloadChunks } from './utils'
import { displayClientChat } from './botUtils'
export interface RendererReactiveState { export interface RendererReactiveState {
world: { world: {
@ -199,13 +197,7 @@ export class AppViewer {
this.currentDisplay = 'world' this.currentDisplay = 'world'
const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0) const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0)
this.worldView = new WorldDataEmitter(world, renderDistance, startPosition) this.worldView = new WorldDataEmitter(world, renderDistance, startPosition)
this.worldView.panicChunksReload = () => { this.worldView.worldRendererConfig = this.inWorldRenderingConfig
if (!options.experimentalClientSelfReload) return
if (process.env.NODE_ENV === 'development') {
displayClientChat(`[client] client panicked due to too long loading time. Soft reloading chunks...`)
}
void reloadChunks()
}
window.worldView = this.worldView window.worldView = this.worldView
watchOptionsAfterWorldViewInit(this.worldView) watchOptionsAfterWorldViewInit(this.worldView)
this.appConfigUdpate() this.appConfigUdpate()
@ -269,6 +261,7 @@ export class AppViewer {
if (cleanState) { if (cleanState) {
this.currentState = undefined this.currentState = undefined
this.currentDisplay = null this.currentDisplay = null
this.worldView?.destroy()
this.worldView = undefined this.worldView = undefined
} }
if (this.backend) { if (this.backend) {

View file

@ -7,12 +7,7 @@ let audioContext: AudioContext
const sounds: Record<string, any> = {} const sounds: Record<string, any> = {}
// Track currently playing sounds and their gain nodes // Track currently playing sounds and their gain nodes
const activeSounds: Array<{ const activeSounds: Array<{ source: AudioBufferSourceNode; gainNode: GainNode; volumeMultiplier: number }> = []
source: AudioBufferSourceNode;
gainNode: GainNode;
volumeMultiplier: number;
isMusic: boolean;
}> = []
window.activeSounds = activeSounds 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 // 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
@ -48,7 +43,7 @@ export async function loadSound (path: string, contents = path) {
} }
} }
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false, isMusic = false) => { export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) => {
const soundBuffer = sounds[url] const soundBuffer = sounds[url]
if (!soundBuffer) { if (!soundBuffer) {
const start = Date.now() const start = Date.now()
@ -56,11 +51,11 @@ export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = option
if (cancelled || Date.now() - start > loadTimeout) return if (cancelled || Date.now() - start > loadTimeout) return
} }
return playSound(url, soundVolume, loop, isMusic) return playSound(url, soundVolume)
} }
export async function playSound (url, soundVolume = 1, loop = false, isMusic = false) { export async function playSound (url, soundVolume = 1) {
const volume = soundVolume * (options.volume / 100) * (isMusic ? options.musicVolume / 100 : 1) const volume = soundVolume * (options.volume / 100)
if (!volume) return if (!volume) return
@ -80,14 +75,13 @@ export async function playSound (url, soundVolume = 1, loop = false, isMusic = f
const gainNode = audioContext.createGain() const gainNode = audioContext.createGain()
const source = audioContext.createBufferSource() const source = audioContext.createBufferSource()
source.buffer = soundBuffer source.buffer = soundBuffer
source.loop = loop
source.connect(gainNode) source.connect(gainNode)
gainNode.connect(audioContext.destination) gainNode.connect(audioContext.destination)
gainNode.gain.value = volume gainNode.gain.value = volume
source.start(0) source.start(0)
// Add to active sounds // Add to active sounds
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume, isMusic }) activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume })
const callbacks = [] as Array<() => void> const callbacks = [] as Array<() => void>
source.onended = () => { source.onended = () => {
@ -105,17 +99,6 @@ export async function playSound (url, soundVolume = 1, loop = false, isMusic = f
onEnded (callback: () => void) { onEnded (callback: () => void) {
callbacks.push(callback) callbacks.push(callback)
}, },
stop () {
try {
source.stop()
// Remove from active sounds
const index = activeSounds.findIndex(s => s.source === source)
if (index !== -1) activeSounds.splice(index, 1)
} catch (err) {
console.warn('Failed to stop sound:', err)
}
},
gainNode,
} }
} }
@ -130,24 +113,11 @@ export function stopAllSounds () {
activeSounds.length = 0 activeSounds.length = 0
} }
export function stopSound (url: string) { export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
const soundIndex = activeSounds.findIndex(s => s.source.buffer === sounds[url])
if (soundIndex !== -1) {
const { source } = activeSounds[soundIndex]
try {
source.stop()
} catch (err) {
console.warn('Failed to stop sound:', err)
}
activeSounds.splice(soundIndex, 1)
}
}
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number, newMusicVolume: number) {
const normalizedVolume = newVolume / 100 const normalizedVolume = newVolume / 100
for (const { gainNode, volumeMultiplier, isMusic } of activeSounds) { for (const { gainNode, volumeMultiplier } of activeSounds) {
try { try {
gainNode.gain.value = normalizedVolume * volumeMultiplier * (isMusic ? newMusicVolume / 100 : 1) gainNode.gain.value = normalizedVolume * volumeMultiplier
} catch (err) { } catch (err) {
console.warn('Failed to change sound volume:', err) console.warn('Failed to change sound volume:', err)
} }
@ -155,9 +125,5 @@ export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number, newMusi
} }
subscribeKey(options, 'volume', () => { subscribeKey(options, 'volume', () => {
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume) changeVolumeOfCurrentlyPlayingSounds(options.volume)
})
subscribeKey(options, 'musicVolume', () => {
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
}) })

View file

@ -118,14 +118,6 @@ export const formatMessage = (message: MessageInput, mcData: IndexedData = globa
return msglist 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 = { const blockToItemRemaps = {
water: 'water_bucket', water: 'water_bucket',
lava: 'lava_bucket', lava: 'lava_bucket',

View file

@ -3,6 +3,7 @@
import MinecraftData from 'minecraft-data' import MinecraftData from 'minecraft-data'
import PrismarineBlock from 'prismarine-block' import PrismarineBlock from 'prismarine-block'
import PrismarineItem from 'prismarine-item' import PrismarineItem from 'prismarine-item'
import pathfinder from 'mineflayer-pathfinder'
import { miscUiState } from './globalState' import { miscUiState } from './globalState'
import supportedVersions from './supportedVersions.mjs' import supportedVersions from './supportedVersions.mjs'
import { options } from './optionsStorage' import { options } from './optionsStorage'
@ -64,6 +65,7 @@ export const loadMinecraftData = async (version: string) => {
window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!) window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!)
window.loadedData = mcData window.loadedData = mcData
window.mcData = mcData window.mcData = mcData
window.pathfinder = pathfinder
miscUiState.loadedDataVersion = version miscUiState.loadedDataVersion = version
} }

View file

@ -818,11 +818,6 @@ export const f3Keybinds: Array<{
} }
] ]
export const reloadChunksAction = () => {
const action = f3Keybinds.find(f3Keybind => f3Keybind.key === 'KeyA')
void action!.action()
}
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (!isGameActive(false)) return if (!isGameActive(false)) return
if (contro.pressedKeys.has('F3')) { if (contro.pressedKeys.has('F3')) {
@ -992,17 +987,14 @@ export function updateBinds (commands: any) {
} }
export const onF3LongPress = async () => { export const onF3LongPress = async () => {
const actions = f3Keybinds.filter(f3Keybind => { const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => {
return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true) return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)
}) }).map(f3Keybind => {
const actionNames = actions.map(f3Keybind => {
return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}` return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}`
}) }))
const select = await showOptionsModal('', actionNames)
if (!select) return if (!select) return
const actionIndex = actionNames.indexOf(select) const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select)
const f3Keybind = actions[actionIndex]! if (f3Keybind) void f3Keybind.action()
void f3Keybind.action()
} }
export const handleMobileButtonCustomAction = (action: CustomAction) => { export const handleMobileButtonCustomAction = (action: CustomAction) => {

View file

@ -1,106 +0,0 @@
import { proxy } from 'valtio'
export const ideState = proxy({
id: '',
contents: '',
line: 0,
column: 0,
language: 'typescript',
title: '',
})
globalThis.ideState = ideState
export const registerIdeChannels = () => {
registerIdeOpenChannel()
registerIdeSaveChannel()
}
const registerIdeOpenChannel = () => {
const CHANNEL_NAME = 'minecraft-web-client:ide-open'
const packetStructure = [
'container',
[
{
name: 'id',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'language',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'contents',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'line',
type: 'i32'
},
{
name: 'column',
type: 'i32'
},
{
name: 'title',
type: ['pstring', { countType: 'i16' }]
}
]
]
bot._client.registerChannel(CHANNEL_NAME, packetStructure, true)
bot._client.on(CHANNEL_NAME as any, (data) => {
const { id, language, contents, line, column, title } = data
ideState.contents = contents
ideState.line = line
ideState.column = column
ideState.id = id
ideState.language = language || 'typescript'
ideState.title = title
})
console.debug(`registered custom channel ${CHANNEL_NAME} channel`)
}
const IDE_SAVE_CHANNEL_NAME = 'minecraft-web-client:ide-save'
const registerIdeSaveChannel = () => {
const packetStructure = [
'container',
[
{
name: 'id',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'contents',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'language',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'line',
type: 'i32'
},
{
name: 'column',
type: 'i32'
},
]
]
bot._client.registerChannel(IDE_SAVE_CHANNEL_NAME, packetStructure, true)
}
export const saveIde = () => {
bot._client.writeChannel(IDE_SAVE_CHANNEL_NAME, {
id: ideState.id,
contents: ideState.contents,
language: ideState.language,
// todo: reflect updated
line: ideState.line,
column: ideState.column,
})
}

View file

@ -2,20 +2,19 @@ import PItem from 'prismarine-item'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
import { options } from './optionsStorage' import { options } from './optionsStorage'
import { jeiCustomCategories } from './inventoryWindows' import { jeiCustomCategories } from './inventoryWindows'
import { registerIdeChannels } from './core/ideChannels'
export default () => { export default () => {
customEvents.on('mineflayerBotCreated', async () => { customEvents.on('mineflayerBotCreated', async () => {
if (!options.customChannels) return if (!options.customChannels) return
bot.once('login', () => { await new Promise(resolve => {
registerBlockModelsChannel() bot.once('login', () => {
registerMediaChannels() resolve(true)
registerSectionAnimationChannels() })
registeredJeiChannel()
registerBlockInteractionsCustomizationChannel()
registerWaypointChannels()
registerIdeChannels()
}) })
registerBlockModelsChannel()
registerMediaChannels()
registerSectionAnimationChannels()
registeredJeiChannel()
}) })
} }
@ -33,95 +32,6 @@ const registerChannel = (channelName: string, packetStructure: any[], handler: (
console.debug(`registered custom channel ${channelName} channel`) console.debug(`registered custom channel ${channelName} channel`)
} }
const registerBlockInteractionsCustomizationChannel = () => {
const CHANNEL_NAME = 'minecraft-web-client:block-interactions-customization'
const packetStructure = [
'container',
[
{
name: 'newConfiguration',
type: ['pstring', { countType: 'i16' }]
},
]
]
registerChannel(CHANNEL_NAME, packetStructure, (data) => {
const config = JSON.parse(data.newConfiguration)
bot.mouse.setConfigFromPacket(config)
}, true)
}
const registerWaypointChannels = () => {
const packetStructure = [
'container',
[
{
name: 'id',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'x',
type: 'f32'
},
{
name: 'y',
type: 'f32'
},
{
name: 'z',
type: 'f32'
},
{
name: 'minDistance',
type: 'i32'
},
{
name: 'label',
type: ['pstring', { countType: 'i16' }]
},
{
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,
metadata
})
})
registerChannel('minecraft-web-client:waypoint-delete', [
'container',
[
{
name: 'id',
type: ['pstring', { countType: 'i16' }]
}
]
], (data) => {
getThreeJsRendererMethods()?.removeWaypoint(data.id)
})
}
const registerBlockModelsChannel = () => { const registerBlockModelsChannel = () => {
const CHANNEL_NAME = 'minecraft-web-client:blockmodels' const CHANNEL_NAME = 'minecraft-web-client:blockmodels'

View file

@ -1,7 +1,6 @@
//@ts-check
import * as nbt from 'prismarine-nbt'
import { options } from './optionsStorage' import { options } from './optionsStorage'
//@ts-check
const { EventEmitter } = require('events') const { EventEmitter } = require('events')
const debug = require('debug')('minecraft-protocol') const debug = require('debug')('minecraft-protocol')
const states = require('minecraft-protocol/src/states') const states = require('minecraft-protocol/src/states')
@ -52,20 +51,8 @@ class CustomChannelClient extends EventEmitter {
this.emit('state', newProperty, oldProperty) this.emit('state', newProperty, oldProperty)
} }
end(endReason, fullReason) { end(reason) {
// eslint-disable-next-line unicorn/no-this-assignment this._endReason = reason
const client = this
if (client.state === states.PLAY) {
fullReason ||= loadedData.supportFeature('chatPacketsUseNbtComponents')
? nbt.comp({ text: nbt.string(endReason) })
: JSON.stringify({ text: endReason })
client.write('kick_disconnect', { reason: fullReason })
} else if (client.state === states.LOGIN) {
fullReason ||= JSON.stringify({ text: endReason })
client.write('disconnect', { reason: fullReason })
}
this._endReason = endReason
this.emit('end', this._endReason) // still emits on server side only, doesn't send anything to our client this.emit('end', this._endReason) // still emits on server side only, doesn't send anything to our client
} }

46
src/dayCycle.ts Normal file
View file

@ -0,0 +1,46 @@
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.dayCycle ? 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.45)
// }
}
bot.on('time', timeUpdated)
timeUpdated()
}

View file

@ -16,8 +16,7 @@ export const defaultOptions = {
chatOpacityOpened: 100, chatOpacityOpened: 100,
messagesLimit: 200, messagesLimit: 200,
volume: 50, volume: 50,
enableMusic: true, enableMusic: false,
musicVolume: 50,
// fov: 70, // fov: 70,
fov: 75, fov: 75,
defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front', defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front',
@ -37,12 +36,11 @@ export const defaultOptions = {
/** @unstable */ /** @unstable */
debugLogNotFrequentPackets: false, debugLogNotFrequentPackets: false,
unimplementedContainers: false, unimplementedContainers: false,
dayCycleAndLighting: true, dayCycle: true,
loadPlayerSkins: true, loadPlayerSkins: true,
renderEars: true, renderEars: true,
lowMemoryMode: false, lowMemoryMode: false,
starfieldRendering: true, starfieldRendering: true,
defaultSkybox: true,
enabledResourcepack: null as string | null, enabledResourcepack: null as string | null,
useVersionsTextures: 'latest', useVersionsTextures: 'latest',
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never', serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
@ -79,17 +77,13 @@ export const defaultOptions = {
frameLimit: false as number | false, frameLimit: false as number | false,
alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null, alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null,
alwaysShowMobileControls: false, alwaysShowMobileControls: false,
excludeCommunicationDebugEvents: [] as string[], excludeCommunicationDebugEvents: [],
preventDevReloadWhilePlaying: false, preventDevReloadWhilePlaying: false,
numWorkers: 4, numWorkers: 4,
localServerOptions: { localServerOptions: {
gameMode: 1 gameMode: 1
} as any, } as any,
saveLoginPassword: 'prompt' as 'prompt' | 'never' | 'always',
preferLoadReadonly: false, preferLoadReadonly: false,
experimentalClientSelfReload: false,
remoteSoundsSupport: false,
remoteSoundsLoadTimeout: 500,
disableLoadPrompts: false, disableLoadPrompts: false,
guestUsername: 'guest', guestUsername: 'guest',
askGuestName: true, askGuestName: true,
@ -98,8 +92,16 @@ export const defaultOptions = {
showCursorBlockInSpectator: false, showCursorBlockInSpectator: false,
renderEntities: true, renderEntities: true,
smoothLighting: true, smoothLighting: true,
newVersionsLighting: false,
chatSelect: true, chatSelect: true,
// experimentalLighting: IS_BETA_TESTER,
experimentalLightingV1: false,
/**
* Controls how lighting is calculated and rendered:
* - 'always-client': Always use client-side lighting engine for all light calculations
* - 'prefer-server': Use server lighting data when available, fallback to client-side calculations
* - 'always-server': Only use lighting data from the server, disable client-side calculations
*/
lightingStrategy: 'prefer-server' as 'always-client' | 'prefer-server' | 'always-server',
autoJump: 'auto' as 'auto' | 'always' | 'never', autoJump: 'auto' as 'auto' | 'always' | 'never',
autoParkour: false, autoParkour: false,
vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users

View file

@ -5,17 +5,6 @@ import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree'
import { enable, disable, enabled } from 'debug' import { enable, disable, enabled } from 'debug'
import { Vec3 } from 'vec3' 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.Vec3 = Vec3
window.cursorBlockRel = (x = 0, y = 0, z = 0) => { window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z) const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)
@ -266,6 +255,7 @@ function connectWebSocket () {
const wsUrl = getWebSocketUrl() const wsUrl = getWebSocketUrl()
if (!wsUrl) { if (!wsUrl) {
console.log('WebSocket server not configured')
return return
} }

View file

@ -11,12 +11,6 @@ export const getFixedFilesize = (bytes: number) => {
return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
} }
export const isInterestedInDownload = () => {
const { map, texturepack, replayFileUrl } = appQueryParams
const { mapDir } = appQueryParamsArray
return !!map || !!texturepack || !!replayFileUrl || !!mapDir
}
const inner = async () => { const inner = async () => {
const { map, texturepack, replayFileUrl } = appQueryParams const { map, texturepack, replayFileUrl } = appQueryParams
const { mapDir } = appQueryParamsArray const { mapDir } = appQueryParamsArray

View file

@ -3,7 +3,6 @@ import fs from 'fs'
import * as nbt from 'prismarine-nbt' import * as nbt from 'prismarine-nbt'
import RegionFile from 'prismarine-provider-anvil/src/region' import RegionFile from 'prismarine-provider-anvil/src/region'
import { versions } from 'minecraft-data' import { versions } from 'minecraft-data'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
import { openWorldDirectory, openWorldZip } from './browserfs' import { openWorldDirectory, openWorldZip } from './browserfs'
import { isGameActive } from './globalState' import { isGameActive } from './globalState'
import { showNotification } from './react/NotificationProvider' import { showNotification } from './react/NotificationProvider'
@ -13,9 +12,6 @@ const parseNbt = promisify(nbt.parse)
const simplifyNbt = nbt.simplify const simplifyNbt = nbt.simplify
window.nbt = nbt window.nbt = nbt
// Supported image types for skybox
const VALID_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp']
// todo display drop zone // todo display drop zone
for (const event of ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop']) { for (const event of ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop']) {
window.addEventListener(event, (e: any) => { window.addEventListener(event, (e: any) => {
@ -49,34 +45,6 @@ window.addEventListener('drop', async e => {
}) })
async function handleDroppedFile (file: File) { async function handleDroppedFile (file: File) {
// Check for image files first when game is active
if (isGameActive(false) && VALID_IMAGE_EXTENSIONS.some(ext => file.name.toLowerCase().endsWith(ext))) {
try {
// Convert image to base64
const reader = new FileReader()
const base64Promise = new Promise<string>((resolve, reject) => {
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
})
reader.readAsDataURL(file)
const base64Image = await base64Promise
// Get ThreeJS backend methods and update skybox
const setSkyboxImage = getThreeJsRendererMethods()?.setSkyboxImage
if (setSkyboxImage) {
await setSkyboxImage(base64Image)
showNotification('Skybox updated successfully')
} else {
showNotification('Cannot update skybox - renderer does not support it')
}
return
} catch (err) {
console.error('Failed to update skybox:', err)
showNotification('Failed to update skybox', 'error')
return
}
}
if (file.name.endsWith('.zip')) { if (file.name.endsWith('.zip')) {
void openWorldZip(file) void openWorldZip(file)
return return

View file

@ -126,28 +126,6 @@ customEvents.on('gameLoaded', () => {
if (entityStatus === EntityStatus.HURT) { if (entityStatus === EntityStatus.HURT) {
getThreeJsRendererMethods()?.damageEntity(entityId, entityStatus) getThreeJsRendererMethods()?.damageEntity(entityId, entityStatus)
} }
if (entityStatus === EntityStatus.BURNED) {
updateEntityStates(entityId, true, true)
}
})
// on fire events
bot._client.on('entity_metadata', (data) => {
if (data.entityId !== bot.entity.id) return
handleEntityMetadata(data)
})
bot.on('end', () => {
if (onFireTimeout) {
clearTimeout(onFireTimeout)
}
})
bot.on('respawn', () => {
if (onFireTimeout) {
clearTimeout(onFireTimeout)
}
}) })
const updateCamera = (entity: Entity) => { const updateCamera = (entity: Entity) => {
@ -246,29 +224,22 @@ customEvents.on('gameLoaded', () => {
} }
} }
// even if not found, still record to cache // 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) { } catch (err) {
reportError(new Error('Error applying skin texture:', { cause: err })) console.error('Error decoding player texture:', err)
} }
} }
bot.on('playerJoined', updateSkin) bot.on('playerJoined', updateSkin)
bot.on('playerUpdated', updateSkin) bot.on('playerUpdated', updateSkin)
for (const entity of Object.values(bot.players)) {
updateSkin(entity)
}
const teamUpdated = (team: Team) => { bot.on('teamUpdated', (team: Team) => {
for (const entity of Object.values(bot.entities)) { 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)) { if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) {
bot.emit('entityUpdate', entity) bot.emit('entityUpdate', entity)
} }
} }
} })
bot.on('teamUpdated', teamUpdated)
for (const team of Object.values(bot.teams)) {
teamUpdated(team)
}
const updateEntityNameTags = (team: Team) => { const updateEntityNameTags = (team: Team) => {
for (const entity of Object.values(bot.entities)) { for (const entity of Object.values(bot.entities)) {
@ -317,7 +288,7 @@ customEvents.on('gameLoaded', () => {
}) })
bot.on('teamRemoved', (team: Team) => { bot.on('teamRemoved', (team: Team) => {
if (appViewer.playerState.reactive.team?.team === team?.team) { if (appViewer.playerState.reactive.team?.team === team.team) {
appViewer.playerState.reactive.team = undefined appViewer.playerState.reactive.team = undefined
// Player's team was removed, need to update all entities that are in a team // Player's team was removed, need to update all entities that are in a team
updateEntityNameTags(team) updateEntityNameTags(team)
@ -325,44 +296,3 @@ customEvents.on('gameLoaded', () => {
}) })
}) })
// Constants
const SHARED_FLAGS_KEY = 0
const ENTITY_FLAGS = {
ON_FIRE: 0x01, // Bit 0
SNEAKING: 0x02, // Bit 1
SPRINTING: 0x08, // Bit 3
SWIMMING: 0x10, // Bit 4
INVISIBLE: 0x20, // Bit 5
GLOWING: 0x40, // Bit 6
FALL_FLYING: 0x80 // Bit 7 (elytra flying)
}
let onFireTimeout: NodeJS.Timeout | undefined
const updateEntityStates = (entityId: number, onFire: boolean, timeout?: boolean) => {
if (entityId !== bot.entity.id) return
appViewer.playerState.reactive.onFire = onFire
if (onFireTimeout) {
clearTimeout(onFireTimeout)
}
if (timeout) {
onFireTimeout = setTimeout(() => {
updateEntityStates(entityId, false, false)
}, 5000)
}
}
// Process entity metadata packet
function handleEntityMetadata (packet: { entityId: number, metadata: Array<{ key: number, type: string, value: number }> }) {
const { entityId, metadata } = packet
// Find shared flags in metadata
const flagsData = metadata.find(meta => meta.key === SHARED_FLAGS_KEY &&
meta.type === 'byte')
// Update fire state if flags were found
if (flagsData) {
const wasOnFire = appViewer.playerState.reactive.onFire
appViewer.playerState.reactive.onFire = (flagsData.value & ENTITY_FLAGS.ON_FIRE) !== 0
}
}

20
src/env.d.ts vendored
View file

@ -2,36 +2,30 @@ declare namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
// Build configuration // Build configuration
NODE_ENV: 'development' | 'production' NODE_ENV: 'development' | 'production'
MIN_MC_VERSION?: string SINGLE_FILE_BUILD?: string
MAX_MC_VERSION?: string
ALWAYS_COMPRESS_LARGE_DATA?: 'true' | 'false'
SINGLE_FILE_BUILD?: 'true' | 'false'
WS_PORT?: string WS_PORT?: string
DISABLE_SERVICE_WORKER?: 'true' | 'false' DISABLE_SERVICE_WORKER?: string
CONFIG_JSON_SOURCE?: 'BUNDLED' | 'REMOTE' CONFIG_JSON_SOURCE?: 'BUNDLED' | 'REMOTE'
LOCAL_CONFIG_FILE?: string LOCAL_CONFIG_FILE?: string
BUILD_VERSION?: string BUILD_VERSION?: string
// Build internals // GitHub and Vercel related
GITHUB_REPOSITORY?: string GITHUB_REPOSITORY?: string
VERCEL_GIT_REPO_OWNER?: string VERCEL_GIT_REPO_OWNER?: string
VERCEL_GIT_REPO_SLUG?: string VERCEL_GIT_REPO_SLUG?: string
// UI // UI and Features
MAIN_MENU_LINKS?: string MAIN_MENU_LINKS?: string
ALWAYS_MINIMAL_SERVER_UI?: 'true' | 'false'
// App features
ENABLE_COOKIE_STORAGE?: string ENABLE_COOKIE_STORAGE?: string
COOKIE_STORAGE_PREFIX?: string COOKIE_STORAGE_PREFIX?: string
// Build info. Release information // Release information
RELEASE_TAG?: string RELEASE_TAG?: string
RELEASE_LINK?: string RELEASE_LINK?: string
RELEASE_CHANGELOG?: string RELEASE_CHANGELOG?: string
// Build info // Other configurations
DEPS_VERSIONS?: string
INLINED_APP_CONFIG?: string INLINED_APP_CONFIG?: string
GITHUB_URL?: string
} }
} }

View file

@ -29,7 +29,7 @@ import './reactUi'
import { lockUrl, onBotCreate } from './controls' import { lockUrl, onBotCreate } from './controls'
import './dragndrop' import './dragndrop'
import { possiblyCleanHandle } from './browserfs' import { possiblyCleanHandle } from './browserfs'
import downloadAndOpenFile, { isInterestedInDownload } from './downloadAndOpenFile' import downloadAndOpenFile from './downloadAndOpenFile'
import fs from 'fs' import fs from 'fs'
import net, { Socket } from 'net' import net, { Socket } from 'net'
@ -56,12 +56,13 @@ import { isCypress } from './standaloneUtils'
import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer' import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
import defaultServerOptions from './defaultLocalServerOptions' import defaultServerOptions from './defaultLocalServerOptions'
import dayCycle from './dayCycle'
import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack' import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack'
import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer' import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer'
import CustomChannelClient from './customClient' import CustomChannelClient from './customClient'
import { registerServiceWorker } from './serviceWorker' import { registerServiceWorker } from './serviceWorker'
import { appStatusState, lastConnectOptions, quickDevReconnect } from './react/AppStatusProvider' import { appStatusState, lastConnectOptions } from './react/AppStatusProvider'
import { fsState } from './loadSave' import { fsState } from './loadSave'
import { watchFov } from './rendererUtils' import { watchFov } from './rendererUtils'
@ -96,7 +97,6 @@ import { registerOpenBenchmarkListener } from './benchmark'
import { tryHandleBuiltinCommand } from './builtinCommands' import { tryHandleBuiltinCommand } from './builtinCommands'
import { loadingTimerState } from './react/LoadingTimer' import { loadingTimerState } from './react/LoadingTimer'
import { loadPluginsIntoWorld } from './react/CreateWorldProvider' import { loadPluginsIntoWorld } from './react/CreateWorldProvider'
import { getCurrentProxy, getCurrentUsername } from './react/ServersList'
window.debug = debug window.debug = debug
window.beforeRenderFrame = [] window.beforeRenderFrame = []
@ -166,7 +166,6 @@ export async function connect (connectOptions: ConnectOptions) {
}) })
} }
appStatusState.showReconnect = false
loadingTimerState.loading = true loadingTimerState.loading = true
loadingTimerState.start = Date.now() loadingTimerState.start = Date.now()
miscUiState.hasErrors = false miscUiState.hasErrors = false
@ -214,13 +213,8 @@ export async function connect (connectOptions: ConnectOptions) {
const destroyAll = (wasKicked = false) => { const destroyAll = (wasKicked = false) => {
if (ended) return if (ended) return
loadingTimerState.loading = false loadingTimerState.loading = false
const { alwaysReconnect } = appQueryParams if (!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) {
if ((!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) || (alwaysReconnect)) { location.reload()
if (alwaysReconnect === 'quick' || alwaysReconnect === 'fast') {
quickDevReconnect()
} else {
location.reload()
}
} }
errorAbortController.abort() errorAbortController.abort()
ended = true ended = true
@ -235,12 +229,8 @@ export async function connect (connectOptions: ConnectOptions) {
bot.emit('end', '') bot.emit('end', '')
bot.removeAllListeners() bot.removeAllListeners()
bot._client.removeAllListeners() bot._client.removeAllListeners()
bot._client = { //@ts-expect-error TODO?
//@ts-expect-error bot._client = undefined
write (packetName) {
console.warn('Tried to write packet', packetName, 'after bot was destroyed')
}
}
//@ts-expect-error //@ts-expect-error
window.bot = bot = undefined window.bot = bot = undefined
} }
@ -286,10 +276,6 @@ export async function connect (connectOptions: ConnectOptions) {
return return
} }
} }
if (e.reason?.stack?.includes('chrome-extension://')) {
// ignore issues caused by chrome extension
return
}
handleError(e.reason) handleError(e.reason)
}, { }, {
signal: errorAbortController.signal signal: errorAbortController.signal
@ -304,7 +290,7 @@ export async function connect (connectOptions: ConnectOptions) {
if (connectOptions.server && !connectOptions.viewerWsConnect && !parsedServer.isWebSocket) { if (connectOptions.server && !connectOptions.viewerWsConnect && !parsedServer.isWebSocket) {
console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`) 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') ?? ''}` }, artificialDelay: appQueryParams.addPing ? Number(appQueryParams.addPing) : undefined }) net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` } })
} }
const renderDistance = singleplayer ? renderDistanceSingleplayer : multiplayerRenderDistance const renderDistance = singleplayer ? renderDistanceSingleplayer : multiplayerRenderDistance
@ -793,6 +779,7 @@ export async function connect (connectOptions: ConnectOptions) {
} }
initMotionTracking() initMotionTracking()
dayCycle()
// Bot position callback // Bot position callback
const botPosition = () => { const botPosition = () => {
@ -893,7 +880,37 @@ export async function connect (connectOptions: ConnectOptions) {
} }
} }
const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined
listenGlobalEvents() listenGlobalEvents()
const unsubscribe = subscribe(miscUiState, async () => {
if (miscUiState.fsReady && miscUiState.appConfig) {
unsubscribe()
if (reconnectOptions) {
sessionStorage.removeItem('reconnectOptions')
if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) {
void connect(reconnectOptions.value)
}
} else {
if (appQueryParams.singleplayer === '1' || appQueryParams.sp === '1') {
loadSingleplayer({}, {
worldFolder: undefined,
...appQueryParams.version ? { version: appQueryParams.version } : {}
})
}
if (appQueryParams.loadSave) {
const savePath = `/data/worlds/${appQueryParams.loadSave}`
try {
await fs.promises.stat(savePath)
} catch (err) {
alert(`Save ${savePath} not found`)
return
}
await loadInMemorySave(savePath)
}
}
}
})
// #region fire click event on touch as we disable default behaviors // #region fire click event on touch as we disable default behaviors
let activeTouch: { touch: Touch, elem: HTMLElement, start: number } | undefined let activeTouch: { touch: Touch, elem: HTMLElement, start: number } | undefined
@ -929,148 +946,90 @@ document.body.addEventListener('touchstart', (e) => {
}, { passive: false }) }, { passive: false })
// #endregion // #endregion
// immediate game enter actions: reconnect or URL QS // qs open actions
const maybeEnterGame = () => { if (!reconnectOptions) {
const waitForConfigFsLoad = (fn: () => void) => { downloadAndOpenFile().then((downloadAction) => {
let unsubscribe: () => void | undefined if (downloadAction) return
const checkDone = () => { if (appQueryParams.reconnect && process.env.NODE_ENV === 'development') {
if (miscUiState.fsReady && miscUiState.appConfig) { const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {})
fn()
unsubscribe?.()
return true
}
return false
}
if (!checkDone()) {
const text = miscUiState.appConfig ? 'Loading' : 'Loading config'
setLoadingScreenStatus(text)
unsubscribe = subscribe(miscUiState, checkDone)
}
}
const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined
if (reconnectOptions) {
sessionStorage.removeItem('reconnectOptions')
if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) {
return waitForConfigFsLoad(async () => {
void connect(reconnectOptions.value)
})
}
}
if (appQueryParams.reconnect && localStorage.lastConnectOptions && process.env.NODE_ENV === 'development') {
const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {})
return waitForConfigFsLoad(async () => {
void connect({ void connect({
botVersion: appQueryParams.version ?? undefined, botVersion: appQueryParams.version ?? undefined,
...lastConnect, ...lastConnect,
ip: appQueryParams.ip || undefined ip: appQueryParams.ip || undefined
}) })
}) return
}
if (appQueryParams.singleplayer === '1' || appQueryParams.sp === '1') {
return waitForConfigFsLoad(async () => {
loadSingleplayer({}, {
worldFolder: undefined,
...appQueryParams.version ? { version: appQueryParams.version } : {}
})
})
}
if (appQueryParams.loadSave) {
const enterSave = async () => {
const savePath = `/data/worlds/${appQueryParams.loadSave}`
try {
await fs.promises.stat(savePath)
await loadInMemorySave(savePath)
} catch (err) {
alert(`Save ${savePath} not found`)
}
} }
return waitForConfigFsLoad(enterSave) if (appQueryParams.ip || appQueryParams.proxy) {
} const waitAppConfigLoad = !appQueryParams.proxy
const openServerEditor = () => {
if (appQueryParams.ip || appQueryParams.proxy) { hideModal()
const openServerAction = () => { if (appQueryParams.onlyConnect) {
if (appQueryParams.autoConnect && miscUiState.appConfig?.allowAutoConnect) { showModal({ reactType: 'only-connect-server' })
void connect({ } else {
server: appQueryParams.ip, showModal({ reactType: 'editServer' })
proxy: getCurrentProxy(), }
botVersion: appQueryParams.version ?? undefined,
username: getCurrentUsername()!,
})
return
} }
showModal({ reactType: 'empty' })
setLoadingScreenStatus(undefined) if (waitAppConfigLoad) {
if (appQueryParams.onlyConnect || process.env.ALWAYS_MINIMAL_SERVER_UI === 'true') { const unsubscribe = subscribe(miscUiState, checkCanDisplay)
showModal({ reactType: 'only-connect-server' }) checkCanDisplay()
// eslint-disable-next-line no-inner-declarations
function checkCanDisplay () {
if (miscUiState.appConfig) {
unsubscribe()
openServerEditor()
return true
}
}
} else { } else {
showModal({ reactType: 'editServer' }) openServerEditor()
} }
} }
// showModal({ reactType: 'empty' }) void Promise.resolve().then(() => {
return waitForConfigFsLoad(openServerAction) // try to connect to peer
} const peerId = appQueryParams.connectPeer
const peerOptions = {} as ConnectPeerOptions
if (appQueryParams.connectPeer) { if (appQueryParams.server) {
// try to connect to peer peerOptions.server = appQueryParams.server
const peerId = appQueryParams.connectPeer }
const peerOptions = {} as ConnectPeerOptions const version = appQueryParams.peerVersion
if (appQueryParams.server) { if (peerId) {
peerOptions.server = appQueryParams.server let username: string | null = options.guestUsername
} if (options.askGuestName) username = prompt('Enter your username', username)
const version = appQueryParams.peerVersion if (!username) return
let username: string | null = options.guestUsername options.guestUsername = username
if (options.askGuestName) username = prompt('Enter your username to connect to peer', username) void connect({
if (!username) return username,
options.guestUsername = username botVersion: version || undefined,
void connect({ peerId,
username, peerOptions
botVersion: version || undefined, })
peerId, }
peerOptions
}) })
return
} if (appQueryParams.serversList && !appQueryParams.ip) {
showModal({ reactType: 'serversList' })
if (appQueryParams.viewerConnect) {
void connect({
username: `viewer-${Math.random().toString(36).slice(2, 10)}`,
viewerWsConnect: appQueryParams.viewerConnect,
})
return
}
if (appQueryParams.modal) {
const modals = appQueryParams.modal.split(',')
for (const modal of modals) {
showModal({ reactType: modal })
} }
return
}
if (appQueryParams.serversList && !miscUiState.appConfig?.appParams?.serversList) { const viewerWsConnect = appQueryParams.viewerConnect
// open UI only if it's in URL if (viewerWsConnect) {
showModal({ reactType: 'serversList' }) void connect({
} username: `viewer-${Math.random().toString(36).slice(2, 10)}`,
viewerWsConnect,
})
}
if (isInterestedInDownload()) { if (appQueryParams.modal) {
void downloadAndOpenFile() const modals = appQueryParams.modal.split(',')
} for (const modal of modals) {
showModal({ reactType: modal })
void possiblyHandleStateVariable() }
} }
}, (err) => {
try { console.error(err)
maybeEnterGame() alert(`Something went wrong: ${err}`)
} catch (err) { })
console.error(err)
alert(`Something went wrong: ${err}`)
} }
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
@ -1081,5 +1040,6 @@ if (initialLoader) {
} }
window.pageLoaded = true window.pageLoaded = true
void possiblyHandleStateVariable()
appViewer.waitBackendLoadPromises.push(appStartup()) appViewer.waitBackendLoadPromises.push(appStartup())
registerOpenBenchmarkListener() registerOpenBenchmarkListener()

View file

@ -9,10 +9,8 @@ import PItem, { Item } from 'prismarine-item'
import { versionToNumber } from 'renderer/viewer/common/utils' import { versionToNumber } from 'renderer/viewer/common/utils'
import { getRenamedData } from 'flying-squid/dist/blockRenames' import { getRenamedData } from 'flying-squid/dist/blockRenames'
import PrismarineChatLoader from 'prismarine-chat' import PrismarineChatLoader from 'prismarine-chat'
import * as nbt from 'prismarine-nbt'
import { BlockModel } from 'mc-assets' import { BlockModel } from 'mc-assets'
import { renderSlot } from 'renderer/viewer/three/renderSlot' import { renderSlot } from 'renderer/viewer/three/renderSlot'
import { loadSkinFromUsername } from 'renderer/viewer/lib/utils/skins'
import Generic95 from '../assets/generic_95.png' import Generic95 from '../assets/generic_95.png'
import { appReplacableResources } from './generated/resources' import { appReplacableResources } from './generated/resources'
import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState' import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState'
@ -24,7 +22,6 @@ import { getItemDescription } from './itemsDescriptions'
import { MessageFormatPart } from './chatUtils' import { MessageFormatPart } from './chatUtils'
import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items' import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items'
import { playerState } from './mineflayer/playerState' import { playerState } from './mineflayer/playerState'
import { modelViewerState } from './react/OverlayModelViewer'
const loadedImagesCache = new Map<string, HTMLImageElement | ImageBitmap>() const loadedImagesCache = new Map<string, HTMLImageElement | ImageBitmap>()
const cleanLoadedImagesCache = () => { const cleanLoadedImagesCache = () => {
@ -42,34 +39,6 @@ export const jeiCustomCategories = proxy({
value: [] as Array<{ id: string, categoryTitle: string, items: any[] }> 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 = () => { export const onGameLoad = () => {
version = bot.version version = bot.version
@ -87,23 +56,12 @@ export const onGameLoad = () => {
return type 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) => { bot.on('windowOpen', (win) => {
const implementedWindow = implementedContainersGuiMap[mapWindowType(win.type as string, win.inventoryStart)] const implementedWindow = implementedContainersGuiMap[mapWindowType(win.type as string, win.inventoryStart)]
if (implementedWindow) { if (implementedWindow) {
openWindow(implementedWindow, maybeParseNbtJson(win.title)) openWindow(implementedWindow)
} else if (options.unimplementedContainers) { } else if (options.unimplementedContainers) {
openWindow('ChestWin', maybeParseNbtJson(win.title)) openWindow('ChestWin')
} else { } else {
// todo format // todo format
displayClientChat(`[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}`) displayClientChat(`[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}`)
@ -300,7 +258,6 @@ export const upInventoryItems = (isInventory: boolean, invWindow = lastWindow) =
// inv.pwindow.inv.slots[2].blockData = getBlockData('dirt') // inv.pwindow.inv.slots[2].blockData = getBlockData('dirt')
const customSlots = mapSlots((isInventory ? bot.inventory : bot.currentWindow)!.slots) const customSlots = mapSlots((isInventory ? bot.inventory : bot.currentWindow)!.slots)
invWindow.pwindow.setSlots(customSlots) invWindow.pwindow.setSlots(customSlots)
return customSlots
} }
export const onModalClose = (callback: () => any) => { export const onModalClose = (callback: () => any) => {
@ -397,7 +354,7 @@ const upWindowItemsLocal = () => {
} }
let skipClosePacketSending = false let skipClosePacketSending = false
const openWindow = (type: string | undefined, title: string | any = undefined) => { const openWindow = (type: string | undefined) => {
// if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) { // if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) {
if (activeModalStack.length) { // game is not in foreground, don't close current modal if (activeModalStack.length) { // game is not in foreground, don't close current modal
if (type) { if (type) {
@ -422,16 +379,12 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
miscUiState.displaySearchInput = false miscUiState.displaySearchInput = false
destroyFn() destroyFn()
skipClosePacketSending = false skipClosePacketSending = false
modelViewerState.model = undefined
}) })
if (type === undefined) {
showInventoryPlayer()
}
cleanLoadedImagesCache() cleanLoadedImagesCache()
const inv = openItemsCanvas(type) const inv = openItemsCanvas(type)
inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch
window.inventory = inv window.inventory = inv
const title = bot.currentWindow?.title
const PrismarineChat = PrismarineChatLoader(bot.version) const PrismarineChat = PrismarineChatLoader(bot.version)
try { try {
inv.canvasManager.children[0].customTitleText = title ? inv.canvasManager.children[0].customTitleText = title ?
@ -470,7 +423,6 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
const isRightClick = type === 'rightclick' const isRightClick = type === 'rightclick'
const isLeftClick = type === 'leftclick' const isLeftClick = type === 'leftclick'
if (isLeftClick || isRightClick) { if (isLeftClick || isRightClick) {
modelViewerState.model = undefined
inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item) inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item)
} }
} else { } else {
@ -502,7 +454,6 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
if (freeSlot === null) return if (freeSlot === null) return
void bot.creative.setInventorySlot(freeSlot, item) void bot.creative.setInventorySlot(freeSlot, item)
} else { } else {
modelViewerState.model = undefined
inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0]) inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0])
} }
} }
@ -571,7 +522,7 @@ const getResultingRecipe = (slots: Array<Item | null>, gridRows: number) => {
type Result = RecipeItem | undefined type Result = RecipeItem | undefined
let shapelessResult: Result let shapelessResult: Result
let shapeResult: Result let shapeResult: Result
outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes ?? {})) { outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes)) {
for (const recipeVariant of recipeVariants) { for (const recipeVariant of recipeVariants) {
if ('inShape' in recipeVariant && equals(currentShape, recipeVariant.inShape as number[][])) { if ('inShape' in recipeVariant && equals(currentShape, recipeVariant.inShape as number[][])) {
shapeResult = recipeVariant.result! shapeResult = recipeVariant.result!
@ -599,7 +550,7 @@ const getAllItemRecipes = (itemName: string) => {
const item = loadedData.itemsByName[itemName] const item = loadedData.itemsByName[itemName]
if (!item) return if (!item) return
const itemId = item.id const itemId = item.id
const recipes = loadedData.recipes?.[itemId] const recipes = loadedData.recipes[itemId]
if (!recipes) return if (!recipes) return
const results = [] as Array<{ const results = [] as Array<{
result: Item, result: Item,
@ -644,7 +595,7 @@ const getAllItemUsages = (itemName: string) => {
if (!item) return if (!item) return
const foundRecipeIds = [] as string[] const foundRecipeIds = [] as string[]
for (const [id, recipes] of Object.entries(loadedData.recipes ?? {})) { for (const [id, recipes] of Object.entries(loadedData.recipes)) {
for (const recipe of recipes) { for (const recipe of recipes) {
if ('inShape' in recipe) { if ('inShape' in recipe) {
if (recipe.inShape.some(row => row.includes(item.id))) { if (recipe.inShape.some(row => row.includes(item.id))) {

View file

@ -1,46 +1,13 @@
import net from 'net'
import { Client } from 'minecraft-protocol' import { Client } from 'minecraft-protocol'
import { appQueryParams } from '../appParams' import { appQueryParams } from '../appParams'
import { downloadAllMinecraftData, getVersionAutoSelect } from '../connect' import { downloadAllMinecraftData, getVersionAutoSelect } from '../connect'
import { gameAdditionalState } from '../globalState' import { gameAdditionalState } from '../globalState'
import { ProgressReporter } from '../core/progressReporter' import { ProgressReporter } from '../core/progressReporter'
import { parseServerAddress } from '../parseServerAddress'
import { getCurrentProxy } from '../react/ServersList'
import { pingServerVersion, validatePacket } from './minecraft-protocol-extra' import { pingServerVersion, validatePacket } from './minecraft-protocol-extra'
import { getWebsocketStream } from './websocket-core' import { getWebsocketStream } from './websocket-core'
let lastPacketTime = 0 let lastPacketTime = 0
customEvents.on('mineflayerBotCreated', () => { customEvents.on('mineflayerBotCreated', () => {
// const oldParsePacketBuffer = bot._client.deserializer.parsePacketBuffer
// try {
// const parsed = oldParsePacketBuffer(buffer)
// } catch (err) {
// debugger
// reportError(new Error(`Error parsing packet ${buffer.subarray(0, 30).toString('hex')}`, { cause: err }))
// throw err
// }
// }
class MinecraftProtocolError extends Error {
constructor (message: string, cause?: Error, public data?: any) {
if (data?.customPayload) {
message += ` (Custom payload: ${data.customPayload.channel})`
}
super(message, { cause })
this.name = 'MinecraftProtocolError'
}
}
const onClientError = (err, data) => {
const error = new MinecraftProtocolError(`Minecraft protocol client error: ${err.message}`, err, data)
reportError(error)
}
if (typeof bot._client['_events'].error === 'function') {
// dont report to bot for more explicit error
bot._client['_events'].error = onClientError
} else {
bot._client.on('error' as any, onClientError)
}
// todo move more code here // todo move more code here
if (!appQueryParams.noPacketsValidation) { if (!appQueryParams.noPacketsValidation) {
(bot._client as unknown as Client).on('packet', (data, packetMeta, buffer, fullBuffer) => { (bot._client as unknown as Client).on('packet', (data, packetMeta, buffer, fullBuffer) => {
@ -68,7 +35,7 @@ setInterval(() => {
}, 1000) }, 1000)
export const getServerInfo = async (ip: string, port?: number, preferredVersion = getVersionAutoSelect(), ping = false, progressReporter?: ProgressReporter, setProxyParams?: ProxyParams) => { export const getServerInfo = async (ip: string, port?: number, preferredVersion = getVersionAutoSelect(), ping = false, progressReporter?: ProgressReporter) => {
await downloadAllMinecraftData() await downloadAllMinecraftData()
const isWebSocket = ip.startsWith('ws://') || ip.startsWith('wss://') const isWebSocket = ip.startsWith('ws://') || ip.startsWith('wss://')
let stream let stream
@ -76,8 +43,6 @@ export const getServerInfo = async (ip: string, port?: number, preferredVersion
progressReporter?.setMessage('Connecting to WebSocket server') progressReporter?.setMessage('Connecting to WebSocket server')
stream = (await getWebsocketStream(ip)).mineflayerStream stream = (await getWebsocketStream(ip)).mineflayerStream
progressReporter?.setMessage('WebSocket connected. Ping packet sent, waiting for response') progressReporter?.setMessage('WebSocket connected. Ping packet sent, waiting for response')
} else if (setProxyParams) {
setProxy(setProxyParams)
} }
window.setLoadingMessage = (message?: string) => { window.setLoadingMessage = (message?: string) => {
if (message === undefined) { if (message === undefined) {
@ -94,46 +59,3 @@ export const getServerInfo = async (ip: string, port?: number, preferredVersion
window.setLoadingMessage = undefined window.setLoadingMessage = undefined
}) })
} }
globalThis.debugTestPing = async (ip: string) => {
const parsed = parseServerAddress(ip, false)
const result = await getServerInfo(parsed.host, parsed.port ? Number(parsed.port) : undefined, undefined, true, undefined, { address: getCurrentProxy(), })
console.log('result', result)
return result
}
export const getDefaultProxyParams = () => {
return {
headers: {
Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}`
}
}
}
export type ProxyParams = {
address?: string
headers?: Record<string, string>
}
export const setProxy = (proxyParams: ProxyParams) => {
if (proxyParams.address?.startsWith(':')) {
proxyParams.address = `${location.protocol}//${location.hostname}${proxyParams.address}`
}
if (proxyParams.address && location.port !== '80' && location.port !== '443' && !/:\d+$/.test(proxyParams.address)) {
const https = proxyParams.address.startsWith('https://') || location.protocol === 'https:'
proxyParams.address = `${proxyParams.address}:${https ? 443 : 80}`
}
const parsedProxy = parseServerAddress(proxyParams.address, false)
const proxy = { host: parsedProxy.host, port: parsedProxy.port }
proxyParams.headers ??= getDefaultProxyParams().headers
net['setProxy']({
hostname: proxy.host,
port: proxy.port,
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 }) }, { signal: abortController.signal })
bot.mouse.beforeUpdateChecks = () => { bot.mouse.beforeUpdateChecks = () => {
if (!document.hasFocus() || !isGameActive(true)) { if (!document.hasFocus()) {
// deactive all buttons // deactive all buttons
bot.mouse.buttons.fill(false) bot.mouse.buttons.fill(false)
} }

View file

@ -15,12 +15,9 @@ class CustomDuplex extends Duplex {
} }
export const getWebsocketStream = async (host: string) => { export const getWebsocketStream = async (host: string) => {
const baseProtocol = host.startsWith('ws://') ? 'ws' : 'wss' const baseProtocol = location.protocol === 'https:' ? 'wss' : host.startsWith('ws://') ? 'ws' : 'wss'
const hostClean = host.replace('ws://', '').replace('wss://', '') const hostClean = host.replace('ws://', '').replace('wss://', '')
const hostURL = new URL(`${baseProtocol}://${hostClean}`) const ws = new WebSocket(`${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 => { const clientDuplex = new CustomDuplex(undefined, data => {
ws.send(data) ws.send(data)
}) })

View file

@ -82,19 +82,23 @@ export const guiOptionsScheme: {
custom () { custom () {
return <Category>Experimental</Category> return <Category>Experimental</Category>
}, },
dayCycleAndLighting: { experimentalLightingV1: {
text: 'Day Cycle', text: 'Experimental Lighting',
tooltip: 'Once stable this setting will be removed and always enabled',
}, },
smoothLighting: {}, smoothLighting: {},
newVersionsLighting: { lightingStrategy: {
text: 'Lighting in Newer Versions', values: [
['prefer-server', 'Prefer Server'],
['always-client', 'Always Client'],
['always-server', 'Always Server'],
],
}, },
lowMemoryMode: { lowMemoryMode: {
text: 'Low Memory Mode', text: 'Low Memory Mode',
enableWarning: 'Enabling it will make chunks load ~4x slower. When in the game, app needs to be reloaded to apply this setting.', enableWarning: 'Enabling it will make chunks load ~4x slower. When in the game, app needs to be reloaded to apply this setting.',
}, },
starfieldRendering: {}, starfieldRendering: {},
renderEntities: {},
keepChunksDistance: { keepChunksDistance: {
max: 5, max: 5,
unit: '', unit: '',
@ -480,24 +484,6 @@ export const guiOptionsScheme: {
], ],
sound: [ sound: [
{ volume: {} }, { 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 () { custom () {
return <Button label='Sound Muffler' onClick={() => showModal({ reactType: 'sound-muffler' })} inScreen /> return <Button label='Sound Muffler' onClick={() => showModal({ reactType: 'sound-muffler' })} inScreen />
@ -568,16 +554,6 @@ export const guiOptionsScheme: {
return <Category>Server Connection</Category> return <Category>Server Connection</Category>
}, },
}, },
{
saveLoginPassword: {
tooltip: 'Controls whether to save login passwords for servers in this browser memory.',
values: [
'prompt',
'always',
'never'
]
},
},
{ {
custom () { custom () {
const { serversAutoVersionSelect } = useSnapshot(options) const { serversAutoVersionSelect } = useSnapshot(options)

View file

@ -7,6 +7,8 @@ import { appStorage } from './react/appStorageProvider'
import { miscUiState } from './globalState' import { miscUiState } from './globalState'
import { defaultOptions } from './defaultOptions' import { defaultOptions } from './defaultOptions'
defaultOptions.experimentalLightingV1 = location.hostname.startsWith('lighting.') // todo
const isDev = process.env.NODE_ENV === 'development' const isDev = process.env.NODE_ENV === 'development'
const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {} const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {}
@ -23,6 +25,11 @@ export const disabledSettings = proxy({
}) })
const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => { const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => {
if (options.dayCycleAndLighting) {
delete options.dayCycleAndLighting
options.dayCycle = options.dayCycleAndLighting
}
if (options.highPerformanceGpu) { if (options.highPerformanceGpu) {
options.gpuPreference = 'high-performance' options.gpuPreference = 'high-performance'
delete options.highPerformanceGpu delete options.highPerformanceGpu

View file

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

View file

@ -29,9 +29,10 @@ interface Props {
accounts?: string[] accounts?: string[]
authenticatedAccounts?: number authenticatedAccounts?: number
versions?: string[] versions?: string[]
allowAutoConnect?: boolean
} }
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions }: Props) => { export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => {
const isSmallHeight = !usePassesScaledDimensions(null, 350) const isSmallHeight = !usePassesScaledDimensions(null, 350)
const qsParamName = parseQs ? appQueryParams.name : undefined const qsParamName = parseQs ? appQueryParams.name : undefined
const qsParamIp = parseQs ? appQueryParams.ip : undefined const qsParamIp = parseQs ? appQueryParams.ip : undefined
@ -39,6 +40,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
const qsParamProxy = parseQs ? appQueryParams.proxy : undefined const qsParamProxy = parseQs ? appQueryParams.proxy : undefined
const qsParamUsername = parseQs ? appQueryParams.username : undefined const qsParamUsername = parseQs ? appQueryParams.username : undefined
const qsParamLockConnect = parseQs ? appQueryParams.lockConnect : undefined const qsParamLockConnect = parseQs ? appQueryParams.lockConnect : undefined
const qsParamAutoConnect = parseQs ? appQueryParams.autoConnect : undefined
const parsedQsIp = parseServerAddress(qsParamIp) const parsedQsIp = parseServerAddress(qsParamIp)
const parsedInitialIp = parseServerAddress(initialData?.ip) const parsedInitialIp = parseServerAddress(initialData?.ip)
@ -116,8 +118,14 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
} }
} }
useEffect(() => {
if (qsParamAutoConnect && qsParamIp && qsParamVersion && allowAutoConnect) {
onQsConnect?.(commonUseOptions)
}
}, [])
const displayConnectButton = qsParamIp const displayConnectButton = qsParamIp
const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg', 'wss://play.webmc.fun'] const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg']
// pick random example // pick random example
const example = serverExamples[Math.floor(Math.random() * serverExamples.length)] const example = serverExamples[Math.floor(Math.random() * serverExamples.length)]
@ -223,7 +231,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
Cancel Cancel
</ButtonWrapper> </ButtonWrapper>
<ButtonWrapper type='submit'> <ButtonWrapper type='submit'>
{displayConnectButton ? translate('Save') : <strong>{translate('Save')}</strong>} {displayConnectButton ? 'Save' : <strong>Save</strong>}
</ButtonWrapper> </ButtonWrapper>
</>} </>}
{displayConnectButton && ( {displayConnectButton && (
@ -238,7 +246,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
onQsConnect?.(commonUseOptions) onQsConnect?.(commonUseOptions)
}} }}
> >
<strong>{translate('Connect')}</strong> <strong>Connect</strong>
</ButtonWrapper> </ButtonWrapper>
</div> </div>
)} )}

View file

@ -54,17 +54,6 @@ export const reconnectReload = () => {
} }
} }
export const quickDevReconnect = () => {
if (!lastConnectOptions.value) {
return
}
resetAppStatusState()
window.dispatchEvent(new window.CustomEvent('connect', {
detail: lastConnectOptions.value
}))
}
export default () => { export default () => {
const lastState = useRef(JSON.parse(JSON.stringify(appStatusState))) const lastState = useRef(JSON.parse(JSON.stringify(appStatusState)))
const currentState = useSnapshot(appStatusState) const currentState = useSnapshot(appStatusState)
@ -116,6 +105,13 @@ export default () => {
} }
}, [isOpen]) }, [isOpen])
const reconnect = () => {
resetAppStatusState()
window.dispatchEvent(new window.CustomEvent('connect', {
detail: lastConnectOptions.value
}))
}
useEffect(() => { useEffect(() => {
const controller = new AbortController() const controller = new AbortController()
window.addEventListener('keyup', (e) => { window.addEventListener('keyup', (e) => {
@ -123,7 +119,7 @@ export default () => {
if (activeModalStack.at(-1)?.reactType !== 'app-status') return if (activeModalStack.at(-1)?.reactType !== 'app-status') return
// todo do only if reconnect is possible // todo do only if reconnect is possible
if (e.code !== 'KeyR' || !lastConnectOptions.value) return if (e.code !== 'KeyR' || !lastConnectOptions.value) return
quickDevReconnect() reconnect()
}, { }, {
signal: controller.signal signal: controller.signal
}) })
@ -144,7 +140,7 @@ export default () => {
const account = await showOptionsModal('Choose account to connect with', [...accounts.map(account => account.username), 'Use other account']) const account = await showOptionsModal('Choose account to connect with', [...accounts.map(account => account.username), 'Use other account'])
if (!account) return if (!account) return
lastConnectOptions.value!.authenticatedAccount = accounts.find(acc => acc.username === account) || true lastConnectOptions.value!.authenticatedAccount = accounts.find(acc => acc.username === account) || true
quickDevReconnect() reconnect()
} }
const lastAutoCapturedPackets = getLastAutoCapturedPackets() const lastAutoCapturedPackets = getLastAutoCapturedPackets()
@ -188,7 +184,7 @@ export default () => {
actionsSlot={ actionsSlot={
<> <>
{displayAuthButton && <Button label='Authenticate' onClick={authReconnectAction} />} {displayAuthButton && <Button label='Authenticate' onClick={authReconnectAction} />}
{displayVpnButton && <PossiblyVpnBypassProxyButton reconnect={quickDevReconnect} />} {displayVpnButton && <PossiblyVpnBypassProxyButton reconnect={reconnect} />}
{replayActive && <Button label={`Download Packets Replay ${replayLogger?.contents.split('\n').length}L`} onClick={downloadPacketsReplay} />} {replayActive && <Button label={`Download Packets Replay ${replayLogger?.contents.split('\n').length}L`} onClick={downloadPacketsReplay} />}
{wasDisconnected && lastAutoCapturedPackets && <Button label={`Inspect Last ${lastAutoCapturedPackets} Packets`} onClick={() => downloadAutoCapturedPackets()} />} {wasDisconnected && lastAutoCapturedPackets && <Button label={`Inspect Last ${lastAutoCapturedPackets} Packets`} onClick={() => downloadAutoCapturedPackets()} />}
</> </>

View file

@ -45,7 +45,7 @@ const MessageLine = ({ message, currentPlayerName, chatOpened }: { message: Mess
return <li className={Object.entries(classes).filter(([, val]) => val).map(([name]) => name).join(' ')} data-time={message.timestamp ? new Date(message.timestamp).toLocaleString('en-US', { hour12: false }) : undefined}> return <li className={Object.entries(classes).filter(([, val]) => val).map(([name]) => name).join(' ')} data-time={message.timestamp ? new Date(message.timestamp).toLocaleString('en-US', { hour12: false }) : undefined}>
{message.parts.map((msg, i) => { {message.parts.map((msg, i) => {
// Check if this is a text part that might contain a mention // Check if this is a text part that might contain a mention
if (typeof msg.text === 'string' && currentPlayerName) { if (msg.text && currentPlayerName) {
const parts = msg.text.split(new RegExp(`(@${currentPlayerName})`, 'i')) const parts = msg.text.split(new RegExp(`(@${currentPlayerName})`, 'i'))
if (parts.length > 1) { if (parts.length > 1) {
return parts.map((txtPart, j) => { return parts.map((txtPart, j) => {
@ -125,9 +125,7 @@ export default ({
const chatInput = useRef<HTMLInputElement>(null!) const chatInput = useRef<HTMLInputElement>(null!)
const chatMessages = useRef<HTMLDivElement>(null) const chatMessages = useRef<HTMLDivElement>(null)
const chatHistoryPos = useRef(sendHistoryRef.current.length) const chatHistoryPos = useRef(sendHistoryRef.current.length)
const commandHistoryPos = useRef(0)
const inputCurrentlyEnteredValue = useRef('') const inputCurrentlyEnteredValue = useRef('')
const commandHistoryRef = useRef(sendHistoryRef.current.filter((msg: string) => msg.startsWith('/')))
const { scrollToBottom, isAtBottom, wasAtBottom, currentlyAtBottom } = useScrollBehavior(chatMessages, { messages, opened }) const { scrollToBottom, isAtBottom, wasAtBottom, currentlyAtBottom } = useScrollBehavior(chatMessages, { messages, opened })
const [rightNowAtBottom, setRightNowAtBottom] = useState(false) const [rightNowAtBottom, setRightNowAtBottom] = useState(false)
@ -144,9 +142,6 @@ export default ({
sendHistoryRef.current = newHistory sendHistoryRef.current = newHistory
window.sessionStorage.chatHistory = JSON.stringify(newHistory) window.sessionStorage.chatHistory = JSON.stringify(newHistory)
chatHistoryPos.current = newHistory.length 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) => { const acceptComplete = (item: string) => {
@ -185,21 +180,6 @@ export default ({
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '') 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') => { const auxInputFocus = (direction: 'up' | 'down') => {
chatInput.current.focus() chatInput.current.focus()
if (direction === 'up') { if (direction === 'up') {
@ -223,7 +203,6 @@ export default ({
updateInputValue(chatInputValueGlobal.value) updateInputValue(chatInputValueGlobal.value)
chatInputValueGlobal.value = '' chatInputValueGlobal.value = ''
chatHistoryPos.current = sendHistoryRef.current.length chatHistoryPos.current = sendHistoryRef.current.length
commandHistoryPos.current = commandHistoryRef.current.length
if (!usingTouch) { if (!usingTouch) {
chatInput.current.focus() chatInput.current.focus()
} }
@ -545,19 +524,9 @@ export default ({
onBlur={() => setIsInputFocused(false)} onBlur={() => setIsInputFocused(false)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.code === 'ArrowUp') { if (e.code === 'ArrowUp') {
if (e.altKey) { handleArrowUp()
handleCommandArrowUp()
e.preventDefault()
} else {
handleArrowUp()
}
} else if (e.code === 'ArrowDown') { } else if (e.code === 'ArrowDown') {
if (e.altKey) { handleArrowDown()
handleCommandArrowDown()
e.preventDefault()
} else {
handleArrowDown()
}
} }
if (e.code === 'Tab') { if (e.code === 'Tab') {
if (completionItemsSource.length) { if (completionItemsSource.length) {

View file

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

View file

@ -98,16 +98,13 @@ export default ({
cursor: chunk ? 'pointer' : 'default', cursor: chunk ? 'pointer' : 'default',
position: 'relative', position: 'relative',
width: `${tileSize}px`, width: `${tileSize}px`,
flexDirection: 'column',
height: `${tileSize}px`, height: `${tileSize}px`,
padding: 1,
// pre-wrap // pre-wrap
whiteSpace: 'pre', whiteSpace: 'pre',
}} }}
> >
{relX}, {relZ}{'\n'} {relX}, {relZ}{'\n'}
{chunk?.lines[0]}{'\n'} {chunk?.lines.join('\n')}
<span style={{ fontSize: `${fontSize * 0.8}px` }}>{chunk?.lines[1]}</span>
</div> </div>
) )
})} })}

View file

@ -2,8 +2,6 @@ import { useEffect, useState } from 'react'
import { useUtilsEffect } from '@zardoy/react-util' import { useUtilsEffect } from '@zardoy/react-util'
import { WorldRendererCommon } from 'renderer/viewer/lib/worldrendererCommon' import { WorldRendererCommon } from 'renderer/viewer/lib/worldrendererCommon'
import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree' import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree'
import { Vec3 } from 'vec3'
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
import Screen from './Screen' import Screen from './Screen'
import ChunksDebug, { ChunkDebug } from './ChunksDebug' import ChunksDebug, { ChunkDebug } from './ChunksDebug'
import { useIsModalActive } from './utilsApp' import { useIsModalActive } from './utilsApp'
@ -14,10 +12,6 @@ const Inner = () => {
const [update, setUpdate] = useState(0) const [update, setUpdate] = useState(0)
useUtilsEffect(({ interval }) => { useUtilsEffect(({ interval }) => {
const up = () => {
// setUpdate(u => u + 1)
}
bot.on('chunkColumnLoad', up)
interval( interval(
500, 500,
() => { () => {
@ -26,48 +20,17 @@ const Inner = () => {
setUpdate(u => u + 1) setUpdate(u => u + 1)
} }
) )
return () => {
bot.removeListener('chunkColumnLoad', up)
}
}, []) }, [])
// Track first load time for all chunks
const allLoadTimes = Object.values(worldView!.debugChunksInfo)
.map(chunk => chunk?.loads[0]?.time ?? Infinity)
.filter(time => time !== Infinity)
.sort((a, b) => a - b)
const allSpiralChunks = Object.fromEntries(generateSpiralMatrix(worldView!.viewDistance).map(pos => [`${pos[0]},${pos[1]}`, pos]))
const mapChunk = (key: string, state: ChunkDebug['state']): ChunkDebug => { const mapChunk = (key: string, state: ChunkDebug['state']): ChunkDebug => {
const x = Number(key.split(',')[0])
const z = Number(key.split(',')[1])
const chunkX = Math.floor(x / 16)
const chunkZ = Math.floor(z / 16)
delete allSpiralChunks[`${chunkX},${chunkZ}`]
const chunk = worldView!.debugChunksInfo[key] const chunk = worldView!.debugChunksInfo[key]
const firstLoadTime = chunk?.loads[0]?.time
const loadIndex = firstLoadTime ? allLoadTimes.indexOf(firstLoadTime) + 1 : 0
// const timeSinceFirstLoad = firstLoadTime ? firstLoadTime - allLoadTimes[0] : 0
const timeSinceFirstLoad = firstLoadTime ? firstLoadTime - allLoadTimes[0] : 0
let line = ''
let line2 = ''
if (loadIndex) {
line = `${loadIndex}`
line2 = `${timeSinceFirstLoad}ms`
}
if (chunk?.loads.length > 1) {
line += ` - ${chunk.loads.length}`
}
return { return {
x, x: Number(key.split(',')[0]),
z, z: Number(key.split(',')[1]),
state, state,
lines: [line, line2], lines: [String(chunk?.loads.length ?? 0)],
sidebarLines: [ sidebarLines: [
`loads: ${chunk?.loads?.map(l => `${l.reason} ${l.dataLength} ${l.time}`).join('\n')}`, `loads: ${chunk.loads?.map(l => `${l.reason} ${l.dataLength} ${l.time}`).join('\n')}`,
// `blockUpdates: ${chunk.blockUpdates}`, // `blockUpdates: ${chunk.blockUpdates}`,
], ],
} }
@ -92,22 +55,14 @@ const Inner = () => {
const chunksDone = Object.keys(world.finishedChunks).map(key => mapChunk(key, 'done')) const chunksDone = Object.keys(world.finishedChunks).map(key => mapChunk(key, 'done'))
const chunksWaitingOrder = Object.values(allSpiralChunks).map(([x, z]) => {
const pos = new Vec3(x * 16, 0, z * 16)
if (bot.world.getColumnAt(pos) === null) return null
return mapChunk(`${pos.x},${pos.z}`, 'order-queued')
}).filter(a => !!a)
const allChunks = [ const allChunks = [
...chunksWaitingServer, ...chunksWaitingServer,
...chunksWaitingClient, ...chunksWaitingClient,
...clientProcessingChunks, ...clientProcessingChunks,
...chunksDone, ...chunksDone,
...chunksDoneEmpty, ...chunksDoneEmpty,
...chunksWaitingOrder,
] ]
return <Screen title={`Chunks Debug (avg: ${worldView!.lastChunkReceiveTimeAvg.toFixed(1)}ms)`}> return <Screen title="Chunks Debug">
<ChunksDebug <ChunksDebug
chunks={allChunks} chunks={allChunks}
playerChunk={{ playerChunk={{

View file

@ -32,8 +32,7 @@ export default () => {
const [packetsString, setPacketsString] = useState('') const [packetsString, setPacketsString] = useState('')
const { showDebugHud } = useSnapshot(miscUiState) const { showDebugHud } = useSnapshot(miscUiState)
const [pos, setPos] = useState<{ x: number, y: number, z: number }>({ x: 0, y: 0, z: 0 }) const [pos, setPos] = useState<{ x: number, y: number, z: number }>({ x: 0, y: 0, z: 0 })
const [skyL, setSkyL] = useState(0) const [lightInfo, setLightInfo] = useState<{ sky: number, block: number, info: string }>({ sky: 0, block: 0, info: '-' })
const [blockL, setBlockL] = useState(0)
const [biomeId, setBiomeId] = useState(0) const [biomeId, setBiomeId] = useState(0)
const [day, setDay] = useState(0) const [day, setDay] = useState(0)
const [timeOfDay, setTimeOfDay] = useState(0) const [timeOfDay, setTimeOfDay] = useState(0)
@ -122,9 +121,28 @@ export default () => {
}) })
const freqUpdateInterval = setInterval(() => { const freqUpdateInterval = setInterval(() => {
const lightingEnabled = appViewer.inWorldRenderingConfig.enableLighting
const { clientSideLighting } = appViewer.inWorldRenderingConfig
let info = ''
if (lightingEnabled) {
if (clientSideLighting === 'none') {
info = 'Server Lighting'
} else if (clientSideLighting === 'full') {
info = 'Client Engine'
} else {
info = 'Server + Client Engine'
}
} else {
info = 'Lighting Disabled'
}
setLightInfo({
sky: bot.world.getSkyLight(bot.entity.position),
block: bot.world.getBlockLight(bot.entity.position),
info
})
setPos({ ...bot.entity.position }) setPos({ ...bot.entity.position })
setSkyL(bot.world.getSkyLight(bot.entity.position))
setBlockL(bot.world.getBlockLight(bot.entity.position))
setBiomeId(bot.world.getBiome(bot.entity.position)) setBiomeId(bot.world.getBiome(bot.entity.position))
setDimension(bot.game.dimension) setDimension(bot.game.dimension)
setDay(bot.time.day) setDay(bot.time.day)
@ -182,7 +200,7 @@ export default () => {
<p>Client TPS: {clientTps} {serverTps ? `Server TPS: ${serverTps.value} ${serverTps.frozen ? '(frozen)' : ''}` : ''}</p> <p>Client TPS: {clientTps} {serverTps ? `Server TPS: ${serverTps.value} ${serverTps.frozen ? '(frozen)' : ''}` : ''}</p>
<p>Facing (viewer): {bot.entity.yaw.toFixed(3)} {bot.entity.pitch.toFixed(3)}</p> <p>Facing (viewer): {bot.entity.yaw.toFixed(3)} {bot.entity.pitch.toFixed(3)}</p>
<p>Facing (minecraft): {quadsDescription[minecraftQuad.current]} ({minecraftYaw.current.toFixed(1)} {(bot.entity.pitch * -180 / Math.PI).toFixed(1)})</p> <p>Facing (minecraft): {quadsDescription[minecraftQuad.current]} ({minecraftYaw.current.toFixed(1)} {(bot.entity.pitch * -180 / Math.PI).toFixed(1)})</p>
<p>Light: {blockL} ({skyL} sky)</p> <p>Light: {lightInfo.block} ({lightInfo.sky} sky) ({lightInfo.info})</p>
<p>Biome: minecraft:{loadedData.biomesArray[biomeId]?.name ?? 'unknown biome'}</p> <p>Biome: minecraft:{loadedData.biomesArray[biomeId]?.name ?? 'unknown biome'}</p>
<p>Day: {day} Time: {timeOfDay}</p> <p>Day: {day} Time: {timeOfDay}</p>

View file

@ -1,153 +0,0 @@
/* eslint-disable no-await-in-loop */
import { useSnapshot } from 'valtio'
import { useEffect, useState } from 'react'
import { getLoadedImage } from 'mc-assets/dist/utils'
import { createCanvas } from 'renderer/viewer/lib/utils'
const TEXTURE_UPDATE_INTERVAL = 100 // 5 times per second
export default () => {
const { onFire, perspective } = useSnapshot(appViewer.playerState.reactive)
const [fireTextures, setFireTextures] = useState<string[]>([])
const [currentTextureIndex, setCurrentTextureIndex] = useState(0)
useEffect(() => {
let animationFrameId: number
let lastTextureUpdate = 0
const updateTexture = (timestamp: number) => {
if (onFire && fireTextures.length > 0) {
if (timestamp - lastTextureUpdate >= TEXTURE_UPDATE_INTERVAL) {
setCurrentTextureIndex(prev => (prev + 1) % fireTextures.length)
lastTextureUpdate = timestamp
}
}
animationFrameId = requestAnimationFrame(updateTexture)
}
animationFrameId = requestAnimationFrame(updateTexture)
return () => cancelAnimationFrame(animationFrameId)
}, [onFire, fireTextures])
useEffect(() => {
const loadTextures = async () => {
const fireImageUrls: string[] = []
const { resourcesManager } = appViewer
const { blocksAtlasParser } = resourcesManager
if (!blocksAtlasParser?.atlas?.latest) {
console.warn('FireRenderer: Blocks atlas parser not available')
return
}
const keys = Object.keys(blocksAtlasParser.atlas.latest.textures).filter(key => /^fire_\d+$/.exec(key))
for (const key of keys) {
const textureInfo = blocksAtlasParser.getTextureInfo(key) as { u: number, v: number, width?: number, height?: number }
if (textureInfo) {
const defaultSize = blocksAtlasParser.atlas.latest.tileSize
const imageWidth = blocksAtlasParser.atlas.latest.width
const imageHeight = blocksAtlasParser.atlas.latest.height
const textureWidth = textureInfo.width ?? defaultSize
const textureHeight = textureInfo.height ?? defaultSize
// Create a temporary canvas for the full texture
const tempCanvas = createCanvas(textureWidth, textureHeight)
const tempCtx = tempCanvas.getContext('2d')
if (tempCtx && blocksAtlasParser.latestImage) {
const image = await getLoadedImage(blocksAtlasParser.latestImage)
tempCtx.drawImage(
image,
textureInfo.u * imageWidth,
textureInfo.v * imageHeight,
textureWidth,
textureHeight,
0,
0,
textureWidth,
textureHeight
)
// Create final canvas with only top 20% of the texture
const finalHeight = Math.ceil(textureHeight * 0.4)
const canvas = createCanvas(textureWidth, finalHeight)
const ctx = canvas.getContext('2d')
if (ctx) {
// Draw only the top portion
ctx.drawImage(
tempCanvas,
0,
0, // Start from top
textureWidth,
finalHeight,
0,
0,
textureWidth,
finalHeight
)
const blob = await canvas.convertToBlob()
const url = URL.createObjectURL(blob)
fireImageUrls.push(url)
}
}
}
}
setFireTextures(fireImageUrls)
}
// Load textures initially
if (appViewer.resourcesManager.currentResources) {
void loadTextures()
}
// Set up listener for texture updates
const onAssetsUpdated = () => {
void loadTextures()
}
appViewer.resourcesManager.on('assetsTexturesUpdated', onAssetsUpdated)
// Cleanup
return () => {
appViewer.resourcesManager.off('assetsTexturesUpdated', onAssetsUpdated)
// Cleanup texture URLs
for (const url of fireTextures) URL.revokeObjectURL(url)
}
}, [])
if (!onFire || fireTextures.length === 0 || perspective !== 'first_person') return null
return (
<div
className='fire-renderer-container'
style={{
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
height: '20dvh',
pointerEvents: 'none',
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-end',
overflow: 'hidden',
zIndex: -1
}}
>
<div
style={{
position: 'absolute',
width: '100%',
height: '100%',
backgroundImage: `url(${fireTextures[currentTextureIndex]})`,
backgroundSize: '50% 100%',
backgroundPosition: 'center',
backgroundRepeat: 'repeat-x',
opacity: 0.7,
filter: 'brightness(1.2) contrast(1.2)',
mixBlendMode: 'screen'
}}
/>
</div>
)
}

View file

@ -2,7 +2,6 @@ import { useRef, useEffect } from 'react'
import { subscribe, useSnapshot } from 'valtio' import { subscribe, useSnapshot } from 'valtio'
import { useUtilsEffect } from '@zardoy/react-util' import { useUtilsEffect } from '@zardoy/react-util'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods' import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
import { isItemActivatableMobile } from 'mineflayer-mouse/dist/activatableItemsMobile'
import { options } from '../optionsStorage' import { options } from '../optionsStorage'
import { activeModalStack, isGameActive, miscUiState } from '../globalState' import { activeModalStack, isGameActive, miscUiState } from '../globalState'
import { onCameraMove, CameraMoveEvent } from '../cameraRotationControls' import { onCameraMove, CameraMoveEvent } from '../cameraRotationControls'
@ -78,10 +77,7 @@ function GameInteractionOverlayInner ({
if (options.touchInteractionType === 'classic') { if (options.touchInteractionType === 'classic') {
virtualClickTimeout ??= setTimeout(() => { virtualClickTimeout ??= setTimeout(() => {
virtualClickActive = true virtualClickActive = true
// If held item is activatable, use right click instead of left document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
const heldItemName = bot?.heldItem?.name
const isOnlyActivatable = heldItemName && isItemActivatableMobile(heldItemName, loadedData)
document.dispatchEvent(new MouseEvent('mousedown', { button: isOnlyActivatable ? 2 : 0 }))
}, touchStartBreakingBlockMs) }, touchStartBreakingBlockMs)
} }
} }
@ -154,23 +150,16 @@ function GameInteractionOverlayInner ({
if (virtualClickActive) { if (virtualClickActive) {
// button 0 is left click // button 0 is left click
// If held item is activatable, use right click instead of left document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
const heldItemName = bot?.heldItem?.name
const isOnlyActivatable = heldItemName && isItemActivatableMobile(heldItemName, loadedData)
document.dispatchEvent(new MouseEvent('mouseup', { button: isOnlyActivatable ? 2 : 0 }))
virtualClickActive = false virtualClickActive = false
} else if (!capturedPointer.active.activateCameraMove && (Date.now() - capturedPointer.active.time < touchStartBreakingBlockMs)) { } else if (!capturedPointer.active.activateCameraMove && (Date.now() - capturedPointer.active.time < touchStartBreakingBlockMs)) {
// single click action // single click action
const MOUSE_BUTTON_RIGHT = 2 const MOUSE_BUTTON_RIGHT = 2
const MOUSE_BUTTON_LEFT = 0 const MOUSE_BUTTON_LEFT = 0
const heldItemName = bot?.heldItem?.name
const isOnlyActivatable = heldItemName && isItemActivatableMobile(heldItemName, loadedData)
const gonnaAttack = !!bot.mouse.getCursorState().entity || !!videoCursorInteraction() const gonnaAttack = !!bot.mouse.getCursorState().entity || !!videoCursorInteraction()
// If not attacking entity and item is activatable, use right click for breaking document.dispatchEvent(new MouseEvent('mousedown', { button: gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT }))
const useButton = !gonnaAttack && isOnlyActivatable ? MOUSE_BUTTON_RIGHT : (gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT)
document.dispatchEvent(new MouseEvent('mousedown', { button: useButton }))
bot.mouse.update() bot.mouse.update()
document.dispatchEvent(new MouseEvent('mouseup', { button: useButton })) document.dispatchEvent(new MouseEvent('mouseup', { button: gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT }))
} }
if (screenTouches > 0) { if (screenTouches > 0) {

View file

@ -115,7 +115,7 @@ const HotbarInner = () => {
container.current.appendChild(inv.canvas) container.current.appendChild(inv.canvas)
const upHotbarItems = () => { const upHotbarItems = () => {
if (!appViewer.resourcesManager?.itemsAtlasParser) return if (!appViewer.resourcesManager?.itemsAtlasParser) return
globalThis.debugHotbarItems = upInventoryItems(true, inv) upInventoryItems(true, inv)
} }
canvasManager.canvas.onclick = (e) => { canvasManager.canvas.onclick = (e) => {
@ -127,7 +127,6 @@ const HotbarInner = () => {
} }
} }
globalThis.debugUpHotbarItems = upHotbarItems
upHotbarItems() upHotbarItems()
bot.inventory.on('updateSlot', upHotbarItems) bot.inventory.on('updateSlot', upHotbarItems)
appViewer.resourcesManager.on('assetsTexturesUpdated', upHotbarItems) appViewer.resourcesManager.on('assetsTexturesUpdated', upHotbarItems)
@ -201,28 +200,17 @@ const HotbarInner = () => {
<ItemName itemKey={itemKey} /> <ItemName itemKey={itemKey} />
<Portal> <Portal>
<div <div
className='hotbar-fullscreen-container' className='hotbar' ref={container} style={{
style={{
position: 'fixed', position: 'fixed',
top: 0,
left: 0, left: 0,
width: '100dvw', right: 0,
height: '100dvh',
zIndex: hasModals ? 1 : 8,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
zIndex: hasModals ? 1 : 8,
pointerEvents: 'none', pointerEvents: 'none',
}}> bottom: 'var(--hud-bottom-raw)'
<div }}
className='hotbar' />
ref={container}
style={{
position: 'absolute',
pointerEvents: 'none',
bottom: 'var(--hud-bottom-raw)'
}}
/>
</div>
</Portal> </Portal>
</SharedHudVars> </SharedHudVars>
} }

View file

@ -5,6 +5,7 @@ import { Effect } from 'mineflayer'
import { inGameError } from '../utils' import { inGameError } from '../utils'
import { fsState } from '../loadSave' import { fsState } from '../loadSave'
import { gameAdditionalState, miscUiState } from '../globalState' import { gameAdditionalState, miscUiState } from '../globalState'
import { options } from '../optionsStorage'
import IndicatorEffects, { EffectType, defaultIndicatorsState } from './IndicatorEffects' import IndicatorEffects, { EffectType, defaultIndicatorsState } from './IndicatorEffects'
import { images } from './effectsImages' import { images } from './effectsImages'
@ -66,6 +67,7 @@ export default ({ displayEffects = true, displayIndicators = true }: { displayEf
const { mesherWork } = useSnapshot(appViewer.rendererState).world const { mesherWork } = useSnapshot(appViewer.rendererState).world
const { hasErrors } = useSnapshot(miscUiState) const { hasErrors } = useSnapshot(miscUiState)
const { disabledUiParts } = useSnapshot(options)
const { isReadonly, openReadOperations, openWriteOperations } = useSnapshot(fsState) const { isReadonly, openReadOperations, openWriteOperations } = useSnapshot(fsState)
const { noConnection, poorConnection } = useSnapshot(gameAdditionalState) const { noConnection, poorConnection } = useSnapshot(gameAdditionalState)
const allIndicators: typeof defaultIndicatorsState = { const allIndicators: typeof defaultIndicatorsState = {
@ -120,7 +122,7 @@ export default ({ displayEffects = true, displayIndicators = true }: { displayEf
return <IndicatorEffects return <IndicatorEffects
indicators={allIndicators} indicators={allIndicators}
effects={effects} effects={effects}
displayIndicators={displayIndicators} displayIndicators
displayEffects={displayEffects} displayEffects
/> />
} }

View file

@ -1,58 +0,0 @@
.monaco-editor-container {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 16px;
background-color: rgba(0, 0, 0, 0.5);
}
.monaco-editor-title {
font-size: 20px;
font-weight: bold;
color: #fff;
margin-bottom: 8px;
}
.monaco-editor-wrapper {
position: relative;
width: 100%;
height: 100%;
max-width: 80vw;
max-height: 80vh;
border: 3px solid #000;
background-color: #000;
padding: 3px;
box-shadow: inset 0 0 0 1px #fff, inset 0 0 0 2px #000;
}
.monaco-editor-close {
position: fixed;
top: 16px;
left: 16px;
z-index: 1001;
cursor: pointer;
padding: 8px;
}
@media (max-width: 768px) {
.monaco-editor-container {
padding: 0;
}
.monaco-editor-wrapper {
max-width: 100%;
max-height: 100%;
border-radius: 0;
}
.monaco-editor-close {
top: 8px;
left: 8px;
}
.monaco-editor-title {
/* todo: make it work on mobile */
display: none;
}
}

View file

@ -1,73 +0,0 @@
import { proxy, useSnapshot } from 'valtio'
import { useEffect } from 'react'
import { Editor } from '@monaco-editor/react'
import PixelartIcon, { pixelartIcons } from '../react/PixelartIcon'
import { useIsModalActive } from '../react/utilsApp'
import { showNotification } from '../react/NotificationProvider'
import { hideModal, showModal } from '../globalState'
import { ideState, saveIde } from '../core/ideChannels'
import './MonacoEditor.css'
export default () => {
const { contents, line, column, id, language, title } = useSnapshot(ideState)
const isModalActive = useIsModalActive('monaco-editor')
const bodyFont = getComputedStyle(document.body).fontFamily
useEffect(() => {
if (id && !isModalActive) {
showModal({ reactType: 'monaco-editor' })
}
if (!id && isModalActive) {
hideModal()
}
}, [id])
useEffect(() => {
if (!isModalActive && id) {
try {
saveIde()
} catch (err) {
reportError(err)
showNotification('Failed to save the editor', 'Please try again', true)
}
ideState.id = ''
ideState.contents = ''
}
}, [isModalActive])
if (!isModalActive) return null
return <div className="monaco-editor-container">
<div className="monaco-editor-close">
<PixelartIcon
iconName={pixelartIcons.close}
width={26}
onClick={() => {
hideModal()
}}
/>
</div>
<div className="monaco-editor-title">
{title}
</div>
<div className="monaco-editor-wrapper">
<Editor
height="100%"
width="100%"
language={language}
theme='vs-dark'
line={line}
onChange={(value) => {
ideState.contents = value ?? ''
}}
value={contents}
options={{
fontFamily: bodyFont,
minimap: {
enabled: true,
},
}}
/>
</div>
</div>
}

View file

@ -4,13 +4,11 @@ import { titleCase } from 'title-case'
import { useMemo } from 'react' import { useMemo } from 'react'
import { disabledSettings, options, qsOptions } from '../optionsStorage' import { disabledSettings, options, qsOptions } from '../optionsStorage'
import { hideAllModals, miscUiState } from '../globalState' import { hideAllModals, miscUiState } from '../globalState'
import { reloadChunksAction } from '../controls'
import Button from './Button' import Button from './Button'
import Slider from './Slider' import Slider from './Slider'
import Screen from './Screen' import Screen from './Screen'
import { showOptionsModal } from './SelectOption' import { showOptionsModal } from './SelectOption'
import PixelartIcon, { pixelartIcons } from './PixelartIcon' import PixelartIcon, { pixelartIcons } from './PixelartIcon'
import { reconnectReload } from './AppStatusProvider'
type GeneralItem<T extends string | number | boolean> = { type GeneralItem<T extends string | number | boolean> = {
id?: string id?: string
@ -20,8 +18,7 @@ type GeneralItem<T extends string | number | boolean> = {
tooltip?: string tooltip?: string
// description?: string // description?: string
enableWarning?: string enableWarning?: string
requiresRestart?: boolean willHaveNoEffect?: boolean
requiresChunksReload?: boolean
values?: Array<T | [T, string]> values?: Array<T | [T, string]>
disableIf?: [option: keyof typeof options, value: any] disableIf?: [option: keyof typeof options, value: any]
} }
@ -59,14 +56,7 @@ const useCommonComponentsProps = (item: OptionMeta) => {
} }
} }
const ignoreReloadWarningsCache = new Set<string>() export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'toggle' }> }) => {
export const OptionButton = ({ item, onClick, valueText, cacheKey }: {
item: Extract<OptionMeta, { type: 'toggle' }>,
onClick?: () => void,
valueText?: string,
cacheKey?: string,
}) => {
const { disabledBecauseOfSetting } = useCommonComponentsProps(item) const { disabledBecauseOfSetting } = useCommonComponentsProps(item)
const optionValue = useSnapshot(options)[item.id!] const optionValue = useSnapshot(options)[item.id!]
@ -94,63 +84,40 @@ export const OptionButton = ({ item, onClick, valueText, cacheKey }: {
return <Button return <Button
data-setting={item.id} data-setting={item.id}
label={`${translate(item.text)}: ${translate(valueText ?? valuesTitlesMap[optionValue])}`} label={`${item.text}: ${valuesTitlesMap[optionValue]}`}
// label={`${item.text}:`}
// postLabel={valuesTitlesMap[optionValue]}
onClick={async (event) => { onClick={async (event) => {
if (disabledReason) { if (disabledReason) {
await showOptionsModal(`${translate('The option is not available')}: ${disabledReason}`, []) await showOptionsModal(`The option is unavailable. ${disabledReason}`, [])
return return
} }
if (item.enableWarning && !options[item.id!]) { if (item.enableWarning && !options[item.id!]) {
const result = await showOptionsModal(item.enableWarning, ['Enable']) const result = await showOptionsModal(item.enableWarning, ['Enable'])
if (!result) return if (!result) return
} }
onClick?.() const { values } = item
if (item.id) { if (values) {
const { values } = item const getOptionValue = (arrItem) => {
if (values) { if (typeof arrItem === 'string') {
const getOptionValue = (arrItem) => { return arrItem
if (typeof arrItem === 'string') {
return arrItem
} else {
return arrItem[0]
}
}
const currentIndex = values.findIndex((value) => {
return getOptionValue(value) === optionValue
})
if (currentIndex === -1) {
options[item.id] = getOptionValue(values[0])
} else { } else {
const nextIndex = event.shiftKey return arrItem[0]
? (currentIndex - 1 + values.length) % values.length
: (currentIndex + 1) % values.length
options[item.id] = getOptionValue(values[nextIndex])
} }
}
const currentIndex = values.findIndex((value) => {
return getOptionValue(value) === optionValue
})
if (currentIndex === -1) {
options[item.id!] = getOptionValue(values[0])
} else { } else {
options[item.id] = !options[item.id] const nextIndex = event.shiftKey
} ? (currentIndex - 1 + values.length) % values.length
} : (currentIndex + 1) % values.length
options[item.id!] = getOptionValue(values[nextIndex])
const toCacheKey = cacheKey ?? item.id ?? ''
if (toCacheKey && !ignoreReloadWarningsCache.has(toCacheKey)) {
ignoreReloadWarningsCache.add(toCacheKey)
if (item.requiresRestart) {
const result = await showOptionsModal(translate('The option requires a restart to take effect'), ['Restart', 'I will do it later'], {
cancel: false,
})
if (result) {
reconnectReload()
}
}
if (item.requiresChunksReload) {
const result = await showOptionsModal(translate('The option requires a chunks reload to take effect'), ['Reload', 'I will do it later'], {
cancel: false,
})
if (result) {
reloadChunksAction()
}
} }
} else {
options[item.id!] = !options[item.id!]
} }
}} }}
title={disabledReason ? `${disabledReason} | ${item.tooltip}` : item.tooltip} title={disabledReason ? `${disabledReason} | ${item.tooltip}` : item.tooltip}
@ -161,15 +128,7 @@ export const OptionButton = ({ item, onClick, valueText, cacheKey }: {
/> />
} }
export const OptionSlider = ({ export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slider' }> }) => {
item,
onChange,
valueOverride
}: {
item: Extract<OptionMeta, { type: 'slider' }>
onChange?: (value: number) => void
valueOverride?: number
}) => {
const { disabledBecauseOfSetting } = useCommonComponentsProps(item) const { disabledBecauseOfSetting } = useCommonComponentsProps(item)
const optionValue = useSnapshot(options)[item.id!] const optionValue = useSnapshot(options)[item.id!]
@ -182,7 +141,7 @@ export const OptionSlider = ({
return ( return (
<Slider <Slider
label={item.text!} label={item.text!}
value={valueOverride ?? options[item.id!]} value={options[item.id!]}
data-setting={item.id} data-setting={item.id}
disabledReason={isLocked(item) ? 'qs' : disabledBecauseOfSetting ? `Disabled because ${item.disableIf![0]} is ${item.disableIf![1]}` : item.disabledReason} disabledReason={isLocked(item) ? 'qs' : disabledBecauseOfSetting ? `Disabled because ${item.disableIf![0]} is ${item.disableIf![1]}` : item.disabledReason}
min={item.min} min={item.min}
@ -192,7 +151,6 @@ export const OptionSlider = ({
updateOnDragEnd={item.delayApply} updateOnDragEnd={item.delayApply}
updateValue={(value) => { updateValue={(value) => {
options[item.id!] = value options[item.id!] = value
onChange?.(value)
}} }}
/> />
) )

View file

@ -1,554 +0,0 @@
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

@ -289,6 +289,11 @@ export default () => {
/> />
</div> </div>
) : null} ) : null}
{!lockConnect && <>
<Button className="button" style={{ width: '204px' }} onClick={disconnect}>
{fsState.inMemorySave && !fsState.syncFs && !fsState.isReadonly ? 'Save & Quit' : 'Disconnect & Reset'}
</Button>
</>}
{(noConnection || appConfig?.alwaysReconnectButton) && ( {(noConnection || appConfig?.alwaysReconnectButton) && (
<div className={styles.row}> <div className={styles.row}>
<Button className="button" style={{ width: appConfig?.reportBugButtonWithReconnect ? '98px' : '204px' }} onClick={reconnectReload}> <Button className="button" style={{ width: appConfig?.reportBugButtonWithReconnect ? '98px' : '204px' }} onClick={reconnectReload}>
@ -338,11 +343,6 @@ export default () => {
)} )}
</div> </div>
)} )}
{!lockConnect && <>
<Button className="button" style={{ width: '204px' }} onClick={disconnect}>
{fsState.inMemorySave && !fsState.syncFs && !fsState.isReadonly ? 'Save & Quit' : 'Disconnect & Reset'}
</Button>
</>}
</div> </div>
<LoadingTimer /> <LoadingTimer />
</Screen> </Screen>

View file

@ -1,11 +1,9 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useUtilsEffect } from '@zardoy/react-util' import { useUtilsEffect } from '@zardoy/react-util'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { supportedVersions } from 'minecraft-protocol'
import { versionToNumber } from 'mc-assets/dist/utils'
import { ConnectOptions } from '../connect' import { ConnectOptions } from '../connect'
import { activeModalStack, hideCurrentModal, miscUiState, notHideableModalsWithoutForce, showModal } from '../globalState' import { activeModalStack, hideCurrentModal, miscUiState, notHideableModalsWithoutForce, showModal } from '../globalState'
import appSupportedVersions from '../supportedVersions.mjs' import supportedVersions from '../supportedVersions.mjs'
import { appQueryParams } from '../appParams' import { appQueryParams } from '../appParams'
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi' import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
import { getServerInfo } from '../mineflayer/mc-protocol' import { getServerInfo } from '../mineflayer/mc-protocol'
@ -22,10 +20,6 @@ import Button from './Button'
import { pixelartIcons } from './PixelartIcon' import { pixelartIcons } from './PixelartIcon'
import { showNotification } from './NotificationProvider' import { showNotification } from './NotificationProvider'
const firstProtocolVersion = versionToNumber(supportedVersions[0])
const lastProtocolVersion = versionToNumber(supportedVersions.at(-1)!)
const protocolSupportedVersions = appSupportedVersions.filter(v => versionToNumber(v) >= firstProtocolVersion && versionToNumber(v) <= lastProtocolVersion)
const EXPLICIT_SHARE_SERVER_MODE = false const EXPLICIT_SHARE_SERVER_MODE = false
if (appQueryParams.lockConnect) { if (appQueryParams.lockConnect) {
@ -119,7 +113,6 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
...serversListProvided, ...serversListProvided,
...(customServersList ? [] : (miscUiState.appConfig?.promoteServers ?? [])).map((server): StoreServerItem => ({ ...(customServersList ? [] : (miscUiState.appConfig?.promoteServers ?? [])).map((server): StoreServerItem => ({
ip: server.ip, ip: server.ip,
name: server.name,
versionOverride: server.version, versionOverride: server.version,
description: server.description, description: server.description,
isRecommended: true isRecommended: true
@ -163,23 +156,13 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
const isWebSocket = server.ip.startsWith('ws://') || server.ip.startsWith('wss://') const isWebSocket = server.ip.startsWith('ws://') || server.ip.startsWith('wss://')
let data let data
if (isWebSocket) { if (isWebSocket) {
try { const pingResult = await getServerInfo(server.ip, undefined, undefined, true)
const pingResult = await getServerInfo(server.ip, undefined, undefined, true) console.log('pingResult.fullInfo.description', pingResult.fullInfo.description)
console.log('pingResult.fullInfo.description', pingResult.fullInfo.description) data = {
data = { formattedText: pingResult.fullInfo.description,
formattedText: pingResult.fullInfo.description, textNameRight: `ws ${pingResult.latency}ms`,
icon: pingResult.fullInfo.favicon, textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`,
textNameRight: `ws ${pingResult.latency}ms`, offline: false
textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`,
offline: false
}
} catch (err) {
data = {
formattedText: 'Failed to connect',
textNameRight: '',
textNameRightGrayed: '',
offline: true
}
} }
} else { } else {
data = await fetchServerStatus(server.ip, /* signal */undefined, server.versionOverride) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME data = await fetchServerStatus(server.ip, /* signal */undefined, server.versionOverride) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME
@ -234,6 +217,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
}) })
const editModalJsx = isEditScreenModal ? <AddServerOrConnect const editModalJsx = isEditScreenModal ? <AddServerOrConnect
allowAutoConnect={miscUiState.appConfig?.allowAutoConnect}
placeholders={{ placeholders={{
proxyOverride: getCurrentProxy(), proxyOverride: getCurrentProxy(),
usernameOverride: getCurrentUsername(), usernameOverride: getCurrentUsername(),
@ -270,7 +254,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
} }
dispatchEvent(new CustomEvent('connect', { detail: connectOptions })) dispatchEvent(new CustomEvent('connect', { detail: connectOptions }))
}} }}
versions={protocolSupportedVersions} versions={supportedVersions}
/> : null /> : null
const serversListJsx = <ServersList const serversListJsx = <ServersList

View file

@ -1,5 +1,5 @@
// Slider.tsx // Slider.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react' import React, { useState, useEffect } from 'react'
import styles from './slider.module.css' import styles from './slider.module.css'
import SharedHudVars from './SharedHudVars' import SharedHudVars from './SharedHudVars'
@ -12,7 +12,6 @@ interface Props extends React.ComponentProps<'div'> {
min?: number; min?: number;
max?: number; max?: number;
disabledReason?: string; disabledReason?: string;
throttle?: number | false; // milliseconds, default 100, false to disable
updateValue?: (value: number) => void; updateValue?: (value: number) => void;
updateOnDragEnd?: boolean; updateOnDragEnd?: boolean;
@ -27,24 +26,15 @@ const Slider: React.FC<Props> = ({
min = 0, min = 0,
max = 100, max = 100,
disabledReason, disabledReason,
throttle = 0,
updateOnDragEnd = false, updateOnDragEnd = false,
updateValue, updateValue,
...divProps ...divProps
}) => { }) => {
label = translate(label)
disabledReason = translate(disabledReason)
valueDisplay = typeof valueDisplay === 'string' ? translate(valueDisplay) : valueDisplay
const [value, setValue] = useState(valueProp) const [value, setValue] = useState(valueProp)
const getRatio = (v = value) => Math.max(Math.min((v - min) / (max - min), 1), 0) const getRatio = (v = value) => Math.max(Math.min((v - min) / (max - min), 1), 0)
const [ratio, setRatio] = useState(getRatio()) const [ratio, setRatio] = useState(getRatio())
// Throttling refs
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const lastValueRef = useRef<number>(valueProp)
useEffect(() => { useEffect(() => {
setValue(valueProp) setValue(valueProp)
}, [valueProp]) }, [valueProp])
@ -52,52 +42,14 @@ const Slider: React.FC<Props> = ({
setRatio(getRatio()) setRatio(getRatio())
}, [value, min, max]) }, [value, min, max])
const throttledUpdateValue = useCallback((newValue: number, dragEnd: boolean) => {
if (updateOnDragEnd !== dragEnd) return
if (!updateValue) return
lastValueRef.current = newValue
if (!throttle) {
// No throttling
updateValue(newValue)
return
}
// Clear existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// Set new timeout
timeoutRef.current = setTimeout(() => {
updateValue(lastValueRef.current)
timeoutRef.current = null
}, throttle)
}, [updateValue, updateOnDragEnd, throttle])
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
// Fire the last value immediately on cleanup
if (updateValue && lastValueRef.current !== undefined) {
updateValue(lastValueRef.current)
}
}
}
}, [updateValue])
const fireValueUpdate = (dragEnd: boolean, v = value) => { const fireValueUpdate = (dragEnd: boolean, v = value) => {
throttledUpdateValue(v, dragEnd) if (updateOnDragEnd !== dragEnd) return
updateValue?.(v)
} }
const labelText = `${label}: ${valueDisplay ?? value} ${unit}`
return ( return (
<SharedHudVars> <SharedHudVars>
<div className={`${styles['slider-container']} settings-text-container ${labelText.length > 17 ? 'settings-text-container-long' : ''}`} style={{ width }} {...divProps}> <div className={styles['slider-container']} style={{ width }} {...divProps}>
<input <input
type="range" type="range"
className={styles.slider} className={styles.slider}
@ -124,7 +76,7 @@ const Slider: React.FC<Props> = ({
<div className={styles.disabled} title={disabledReason} /> <div className={styles.disabled} title={disabledReason} />
<div className={styles['slider-thumb']} style={{ left: `calc((100% * ${ratio}) - (8px * ${ratio}))` }} /> <div className={styles['slider-thumb']} style={{ left: `calc((100% * ${ratio}) - (8px * ${ratio}))` }} />
<label className={styles.label}> <label className={styles.label}>
{labelText} {label}: {valueDisplay ?? value} {unit}
</label> </label>
</div> </div>
</SharedHudVars> </SharedHudVars>

View file

@ -1,6 +1,5 @@
import { CSSProperties, PointerEvent, useEffect, useRef, useState } from 'react' import { CSSProperties, PointerEvent, useEffect, useRef, useState } from 'react'
import { proxy, ref, useSnapshot } from 'valtio' import { proxy, ref, useSnapshot } from 'valtio'
import activatableItemsMobile from 'mineflayer-mouse/dist/activatableItemsMobile'
import { contro } from '../controls' import { contro } from '../controls'
import { options } from '../optionsStorage' import { options } from '../optionsStorage'
import PixelartIcon from './PixelartIcon' import PixelartIcon from './PixelartIcon'
@ -73,12 +72,10 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props)
break: false, break: false,
jump: bot?.getControlState('jump'), jump: bot?.getControlState('jump'),
}[name] }[name]
const RIGHT_MOUSE_BUTTON = 2
const LEFT_MOUSE_BUTTON = 0
const holdDown = { const holdDown = {
action () { action () {
if (!bot) return if (!bot) return
document.dispatchEvent(new MouseEvent('mousedown', { button: RIGHT_MOUSE_BUTTON })) document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
bot.mouse.update() bot.mouse.update()
}, },
sneak () { sneak () {
@ -90,7 +87,7 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props)
}, },
break () { break () {
if (!bot) return if (!bot) return
document.dispatchEvent(new MouseEvent('mousedown', { button: LEFT_MOUSE_BUTTON })) document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
bot.mouse.update() bot.mouse.update()
active = true active = true
}, },
@ -104,7 +101,7 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props)
} }
const holdUp = { const holdUp = {
action () { action () {
document.dispatchEvent(new MouseEvent('mouseup', { button: RIGHT_MOUSE_BUTTON })) document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
}, },
sneak () { sneak () {
void contro.emit('release', { void contro.emit('release', {
@ -115,7 +112,7 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props)
}, },
break () { break () {
if (!bot) return if (!bot) return
document.dispatchEvent(new MouseEvent('mouseup', { button: LEFT_MOUSE_BUTTON })) document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
bot.mouse.update() bot.mouse.update()
active = false active = false
}, },

View file

@ -91,14 +91,6 @@ const setCookieValue = (key: string, value: string): boolean => {
} }
document.cookie = cookie document.cookie = cookie
// Verify the cookie was actually saved by reading it back
const savedValue = getCookieValue(key)
if (savedValue !== value) {
console.warn(`Cookie verification failed for key '${key}'. Expected: ${value}, Got: ${savedValue}`)
return false
}
return true return true
} catch (error) { } catch (error) {
console.error(`Failed to set cookie for key '${key}':`, error) console.error(`Failed to set cookie for key '${key}':`, error)
@ -237,19 +229,12 @@ export const getRandomUsername = (appConfig: AppConfig) => {
export const appStorage = proxy({ ...defaultStorageData }) export const appStorage = proxy({ ...defaultStorageData })
// Track if cookies failed in this session
let cookiesFailedThisSession = false
// Check if cookie storage should be used (will be set by options) // Check if cookie storage should be used (will be set by options)
const shouldUseCookieStorage = () => { const shouldUseCookieStorage = () => {
// If cookies failed this session, don't try again const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
if (cookiesFailedThisSession) {
return false
}
const isSecureCookiesAvailable = () => { const isSecureCookiesAvailable = () => {
// either https or localhost // either https or localhost
return window.location.protocol === 'https:' || (window.location.hostname === 'localhost') return window.location.protocol === 'https:' || (window.location.hostname === 'localhost' && !isSafari)
} }
if (!isSecureCookiesAvailable()) { if (!isSecureCookiesAvailable()) {
return false return false
@ -360,10 +345,8 @@ const saveKey = (key: keyof StorageData) => {
// Remove from localStorage if cookie save was successful // Remove from localStorage if cookie save was successful
markLocalStorageAsMigrated(key) markLocalStorageAsMigrated(key)
} else { } else {
// Cookie save failed, disable cookies for this session and fallback to localStorage // Disabling for now so no confusing conflicts modal after page reload
console.warn(`Cookie save failed for key '${key}', disabling cookies for this session`) // useLocalStorage = true
cookiesFailedThisSession = true
useLocalStorage = true
} }
} }
} }

View file

@ -66,9 +66,6 @@ import CreditsAboutModal from './react/CreditsAboutModal'
import GlobalOverlayHints from './react/GlobalOverlayHints' import GlobalOverlayHints from './react/GlobalOverlayHints'
import FullscreenTime from './react/FullscreenTime' import FullscreenTime from './react/FullscreenTime'
import StorageConflictModal from './react/StorageConflictModal' 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' const isFirefox = ua.getBrowser().name === 'Firefox'
if (isFirefox) { if (isFirefox) {
@ -174,7 +171,6 @@ const InGameUi = () => {
<VoiceMicrophone /> <VoiceMicrophone />
<ChunksDebugScreen /> <ChunksDebugScreen />
<RendererDebugMenu /> <RendererDebugMenu />
{!disabledUiParts.includes('fire') && <FireRenderer />}
</PerComponentErrorBoundary> </PerComponentErrorBoundary>
</div> </div>
@ -250,6 +246,7 @@ const App = () => {
<PacketsReplayProvider /> <PacketsReplayProvider />
<NotificationProvider /> <NotificationProvider />
<ModsPage /> <ModsPage />
<SelectOption /> <SelectOption />
<CreditsAboutModal /> <CreditsAboutModal />
<NoModalFoundProvider /> <NoModalFoundProvider />
@ -260,8 +257,6 @@ const App = () => {
</div> </div>
<div /> <div />
<DebugEdges /> <DebugEdges />
<OverlayModelViewer />
<MonacoEditor />
<DebugResponseTimeIndicator /> <DebugResponseTimeIndicator />
</RobustPortal> </RobustPortal>
</ButtonAppProvider> </ButtonAppProvider>

View file

@ -486,6 +486,17 @@ 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 = () => { export const onAppLoad = () => {
customEvents.on('mineflayerBotCreated', () => { customEvents.on('mineflayerBotCreated', () => {
// todo also handle resourcePack // todo also handle resourcePack

View file

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

View file

@ -1,9 +1,13 @@
import { versionToNumber } from 'renderer/viewer/common/utils' import { versionToNumber } from 'renderer/viewer/common/utils'
import { restoreMinecraftData } from '../optimizeJson' import { restoreMinecraftData } from '../optimizeJson'
// import minecraftInitialDataJson from '../../generated/minecraft-initial-data.json' // import minecraftInitialDataJson from '../../generated/minecraft-initial-data.json'
import { toMajorVersion } from '../utils'
import { importLargeData } from '../../generated/large-data-aliases' import { importLargeData } from '../../generated/large-data-aliases'
const toMajorVersion = version => {
const [a, b] = (String(version)).split('.')
return `${a}.${b}`
}
const customResolver = () => { const customResolver = () => {
const resolver = Promise.withResolvers() const resolver = Promise.withResolvers()
let resolvedData let resolvedData
@ -19,6 +23,8 @@ const customResolver = () => {
} }
} }
//@ts-expect-error for workers using minecraft-data
globalThis.window ??= globalThis
let dataStatus = 'not-called' let dataStatus = 'not-called'
const optimizedDataResolver = customResolver() const optimizedDataResolver = customResolver()
@ -75,7 +81,7 @@ const possiblyGetFromCache = (version: string) => {
cacheTime.set(version, Date.now()) cacheTime.set(version, Date.now())
return data return data
} }
window.allLoadedMcData = new Proxy({}, { window.allLoadedMcData ??= new Proxy({}, {
get (t, version: string) { get (t, version: string) {
// special properties like $typeof // special properties like $typeof
if (version.includes('$')) return if (version.includes('$')) return

View file

@ -11,7 +11,6 @@ import { showNotification } from '../react/NotificationProvider'
import { pixelartIcons } from '../react/PixelartIcon' import { pixelartIcons } from '../react/PixelartIcon'
import { createSoundMap, SoundMap } from './soundsMap' import { createSoundMap, SoundMap } from './soundsMap'
import { musicSystem } from './musicSystem' import { musicSystem } from './musicSystem'
import './customSoundSystem'
let soundMap: SoundMap | undefined let soundMap: SoundMap | undefined
@ -51,9 +50,8 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
appViewer.backend?.soundSystem?.playSound( appViewer.backend?.soundSystem?.playSound(
position, position,
soundData.url, soundData.url,
soundData.volume, soundData.volume * (options.volume / 100),
Math.max(Math.min(pitch ?? 1, 2), 0.5), Math.max(Math.min(pitch ?? 1, 2), 0.5)
soundData.timeout ?? options.remoteSoundsLoadTimeout
) )
} }
if (getDistance(bot.entity.position, position) < 4 * 16) { if (getDistance(bot.entity.position, position) < 4 * 16) {
@ -83,7 +81,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
} }
const randomMusicKey = musicKeys[Math.floor(Math.random() * musicKeys.length)] const randomMusicKey = musicKeys[Math.floor(Math.random() * musicKeys.length)]
const soundData = await soundMap.getSoundUrl(randomMusicKey) const soundData = await soundMap.getSoundUrl(randomMusicKey)
if (!soundData || !soundMap) return if (!soundData) return
await musicSystem.playMusic(soundData.url, soundData.volume) await musicSystem.playMusic(soundData.url, soundData.volume)
} }
@ -111,9 +109,6 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
} }
bot.on('soundEffectHeard', async (soundId, position, volume, pitch) => { bot.on('soundEffectHeard', async (soundId, position, volume, pitch) => {
if (/^https?:/.test(soundId.replace('minecraft:', ''))) {
return
}
await playHardcodedSound(soundId, position, volume, pitch) await playHardcodedSound(soundId, position, volume, pitch)
}) })

View file

@ -1,46 +0,0 @@
import { loadOrPlaySound, stopAllSounds, stopSound } from '../basicSounds'
import { options } from '../optionsStorage'
const customSoundSystem = () => {
bot._client.on('named_sound_effect', packet => {
if (!options.remoteSoundsSupport) return
let { soundName } = packet
let metadata = {} as { loadTimeout?: number, loop?: boolean }
// Extract JSON metadata from parentheses at the end
const jsonMatch = /\(({.*})\)$/.exec(soundName)
if (jsonMatch) {
try {
metadata = JSON.parse(jsonMatch[1])
soundName = soundName.slice(0, -jsonMatch[0].length)
} catch (e) {
console.warn('Failed to parse sound metadata:', jsonMatch[1])
}
}
if (/^https?:/.test(soundName.replace('minecraft:', ''))) {
const { loadTimeout, loop } = metadata
void loadOrPlaySound(soundName, packet.volume, loadTimeout, loop)
}
})
bot._client.on('stop_sound', packet => {
const { flags, source, sound } = packet
if (flags === 0) {
// Stop all sounds
stopAllSounds()
} else if (sound) {
// Stop specific sound by name
stopSound(sound)
}
})
bot.on('end', () => {
stopAllSounds()
})
}
customEvents.on('mineflayerBotCreated', () => {
customSoundSystem()
})

View file

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

View file

@ -35,7 +35,6 @@ interface ResourcePackSoundEntry {
name: string name: string
stream?: boolean stream?: boolean
volume?: number volume?: number
timeout?: number
} }
interface ResourcePackSound { interface ResourcePackSound {
@ -141,7 +140,7 @@ export class SoundMap {
await scan(soundsBasePath) await scan(soundsBasePath)
} }
async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number, timeout?: number } | undefined> { async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number } | undefined> {
// First check resource pack sounds.json // First check resource pack sounds.json
if (this.activeResourcePackSoundsJson && soundKey in this.activeResourcePackSoundsJson) { if (this.activeResourcePackSoundsJson && soundKey in this.activeResourcePackSoundsJson) {
const rpSound = this.activeResourcePackSoundsJson[soundKey] const rpSound = this.activeResourcePackSoundsJson[soundKey]
@ -152,13 +151,6 @@ export class SoundMap {
if (this.activeResourcePackBasePath) { if (this.activeResourcePackBasePath) {
const tryFormat = async (format: string) => { const tryFormat = async (format: string) => {
try { try {
if (sound.name.startsWith('http://') || sound.name.startsWith('https://')) {
return {
url: sound.name,
volume: soundVolume * Math.max(Math.min(volume, 1), 0),
timeout: sound.timeout
}
}
const resourcePackPath = path.join(this.activeResourcePackBasePath!, `/assets/minecraft/sounds/${sound.name}.${format}`) const resourcePackPath = path.join(this.activeResourcePackBasePath!, `/assets/minecraft/sounds/${sound.name}.${format}`)
const fileData = await fs.promises.readFile(resourcePackPath) const fileData = await fs.promises.readFile(resourcePackPath)
return { return {

View file

@ -3,7 +3,6 @@
import { subscribeKey } from 'valtio/utils' import { subscribeKey } from 'valtio/utils'
import { isMobile } from 'renderer/viewer/lib/simpleUtils' import { isMobile } from 'renderer/viewer/lib/simpleUtils'
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter' import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
import { setSkinsConfig } from 'renderer/viewer/lib/utils/skins'
import { options, watchValue } from './optionsStorage' import { options, watchValue } from './optionsStorage'
import { reloadChunks } from './utils' import { reloadChunks } from './utils'
import { miscUiState } from './globalState' import { miscUiState } from './globalState'
@ -81,10 +80,6 @@ export const watchOptionsAfterViewerInit = () => {
updateFpsLimit(o) updateFpsLimit(o)
}) })
watchValue(options, o => {
appViewer.inWorldRenderingConfig.volume = Math.max(o.volume / 100, 0)
})
watchValue(options, o => { watchValue(options, o => {
appViewer.inWorldRenderingConfig.vrSupport = o.vrSupport appViewer.inWorldRenderingConfig.vrSupport = o.vrSupport
appViewer.inWorldRenderingConfig.vrPageGameRendering = o.vrPageGameRendering appViewer.inWorldRenderingConfig.vrPageGameRendering = o.vrPageGameRendering
@ -98,8 +93,6 @@ export const watchOptionsAfterViewerInit = () => {
appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor
appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading
appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks
setSkinsConfig({ apiEnabled: o.loadPlayerSkins })
}) })
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
@ -107,22 +100,46 @@ export const watchOptionsAfterViewerInit = () => {
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
}) })
subscribeKey(options, 'newVersionsLighting', () => { const updateLightingStrategy = () => {
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting if (!bot) return
}) if (!options.experimentalLightingV1) {
appViewer.inWorldRenderingConfig.clientSideLighting = 'none'
appViewer.inWorldRenderingConfig.enableLighting = false
appViewer.inWorldRenderingConfig.legacyLighting = true
return
}
const lightingEnabled = options.dayCycle
if (!lightingEnabled) {
appViewer.inWorldRenderingConfig.clientSideLighting = 'none'
appViewer.inWorldRenderingConfig.enableLighting = false
return
}
appViewer.inWorldRenderingConfig.legacyLighting = false
// for now ignore saved lighting to allow proper updates and singleplayer created worlds
// appViewer.inWorldRenderingConfig.flyingSquidWorkarounds = miscUiState.flyingSquid
const serverParsingSupported = miscUiState.flyingSquid ? /* !bot.supportFeature('blockStateId') */false : bot.supportFeature('blockStateId')
const serverLightingPossible = serverParsingSupported && (options.lightingStrategy === 'prefer-server' || options.lightingStrategy === 'always-server')
const clientLightingPossible = options.lightingStrategy !== 'always-server'
const clientSideLighting = !serverLightingPossible
appViewer.inWorldRenderingConfig.clientSideLighting = serverLightingPossible && clientLightingPossible ? 'partial' : clientSideLighting ? 'full' : 'none'
appViewer.inWorldRenderingConfig.enableLighting = serverLightingPossible || clientLightingPossible
}
subscribeKey(options, 'lightingStrategy', updateLightingStrategy)
customEvents.on('mineflayerBotCreated', () => { customEvents.on('mineflayerBotCreated', () => {
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting updateLightingStrategy()
}) })
watchValue(options, o => { watchValue(options, o => {
appViewer.inWorldRenderingConfig.starfield = o.starfieldRendering appViewer.inWorldRenderingConfig.starfield = o.starfieldRendering
}) })
watchValue(options, o => {
appViewer.inWorldRenderingConfig.defaultSkybox = o.defaultSkybox
})
watchValue(options, o => { watchValue(options, o => {
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates // appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
}) })
@ -135,6 +152,5 @@ export const watchOptionsAfterWorldViewInit = (worldView: WorldDataEmitter) => {
appViewer.inWorldRenderingConfig.renderEars = o.renderEars appViewer.inWorldRenderingConfig.renderEars = o.renderEars
appViewer.inWorldRenderingConfig.showHand = o.showHand appViewer.inWorldRenderingConfig.showHand = o.showHand
appViewer.inWorldRenderingConfig.viewBobbing = o.viewBobbing appViewer.inWorldRenderingConfig.viewBobbing = o.viewBobbing
appViewer.inWorldRenderingConfig.dayCycle = o.dayCycleAndLighting
}) })
} }