Compare commits

..

32 commits

Author SHA1 Message Date
Vitaly
c3aad71024
[deploy] send token channel 2025-08-06 18:02:24 +02:00
Vitaly
6324fe50b7
Merge branch 'next' into instancing 2025-08-06 17:38:45 +02:00
Vitaly Turovsky
c63b6bb536 [skip ci] test grid 2025-08-01 03:35:45 +03:00
Vitaly Turovsky
c93b7a6f8b Refactor sign and head container addition in ChunkMeshManager with error handling 2025-07-20 23:02:29 +03:00
Vitaly Turovsky
be0993a00b up all 2025-07-20 10:02:21 +03:00
Vitaly Turovsky
3e5c036512 Merge remote-tracking branch 'origin/next' into instancing 2025-07-20 09:43:22 +03:00
Vitaly Turovsky
370f07712b Merge branch 'next' into instancing 2025-07-20 09:43:03 +03:00
Vitaly Turovsky
0921b40f88 fix signs 2025-07-20 09:38:49 +03:00
Vitaly Turovsky
96c5ebb379 [to pick] fix chat crash 2025-07-20 09:38:22 +03:00
Vitaly Turovsky
2f49cbb35b finish manager! 2025-07-20 09:24:57 +03:00
Vitaly Turovsky
9ee28ef62f working geometry pool manager! 2025-07-20 08:21:15 +03:00
Vitaly Turovsky
0240a752ad box helper optim 2025-07-19 18:11:01 +03:00
Vitaly Turovsky
1c8799242a some important fixes 2025-07-19 18:10:09 +03:00
Vitaly Turovsky
b2257d8ae4 rm unused code 2025-07-16 09:16:06 +03:00
Vitaly Turovsky
5e30a4736e rm cache 2025-07-16 09:14:04 +03:00
Vitaly Turovsky
83018cd828 [before test] refactor to use state id, force! 2025-07-16 09:12:56 +03:00
Vitaly Turovsky
6868068705 fixes 2025-07-16 08:41:05 +03:00
Vitaly Turovsky
dbbe5445d8 realScene test 2025-07-16 07:55:29 +03:00
Vitaly Turovsky
336aad678b real 2025-07-16 07:52:17 +03:00
Vitaly Turovsky
3c358c9d22 all done! 2025-07-16 06:46:50 +03:00
Vitaly Turovsky
7b06561fc7 debug, try to fix growth, fix perf 2025-07-16 06:21:13 +03:00
Vitaly Turovsky
9f29491b5d maybe support rotation 2025-07-16 04:55:43 +03:00
Vitaly Turovsky
dbce9e7bec working texturing 2025-07-16 04:17:25 +03:00
Vitaly Turovsky
6083416943 utils 2025-07-16 03:15:36 +03:00
Vitaly Turovsky
102520233a code cleanup towards text 2025-07-16 03:14:08 +03:00
Vitaly Turovsky
a19d459e8a rm blocks hardcode 2025-07-16 02:16:17 +03:00
Vitaly Turovsky
561a18527f add creative server 2025-07-12 05:38:12 +03:00
Vitaly Turovsky
7ec9d10787 Merge branch 'next' into instancing 2025-07-09 17:19:31 +03:00
Vitaly Turovsky
7a692ac210 f 2025-06-29 15:22:58 +03:00
Vitaly Turovsky
52a90ce8ff Merge branch 'next' into instancing 2025-06-29 15:22:55 +03:00
Vitaly Turovsky
4aebfecf69 some progress 2025-06-29 15:22:21 +03:00
Vitaly Turovsky
97f8061b06 feat: Implement instanced rendering mode for low-end devices and performance testing 2025-06-25 17:25:33 +03:00
90 changed files with 2982 additions and 4312 deletions

View file

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

View file

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

View file

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

View file

@ -38,7 +38,6 @@ All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) ar
- Controls -> **Touch Controls Type** -> **Joystick**
- Controls -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue
- Interface -> **Enable Minimap** -> **Always** - To enable useful minimap (why not?)
- Controls -> **Raw Input** -> **On** - This will make the controls more precise (UPD: already enabled by default)
- Interface -> **Chat Select** -> **On** - To select chat messages (UPD: already enabled by default)
@ -54,7 +53,7 @@ Howerver, it's known that these browsers have issues:
### 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):
- 1.19.4
@ -78,8 +77,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)
> **Note**: If you want to make **your own** Minecraft server accessible to web clients (without our proxies), you can use [mwc-proxy](https://github.com/zardoy/mwc-proxy) - a lightweight JS WebSocket proxy that runs on the same server as your Minecraft server, allowing players to connect directly via `wss://play.example.com`. `?client_mcraft` is added to the URL, so the proxy will know that it's this client.
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
```mermaid
@ -178,7 +175,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.
- `?autoConnect=true` - Only works then `ip` and `version` parameters are set and `allowAutoConnect` is `true` in config.json! Directly connects to the specified server. Useful for integrates iframes.
- `?serversList=<list_or_url>` - `<list_or_url>` can be a list of servers in the format `ip:version,ip` or a url to a json file with the same format (array) or a txt file with line-delimited list of server IPs.
- `?addPing=<ping>` - Add a latency to both sides of the connection. Useful for testing ping issues. For example `?addPing=100` will add 200ms to your ping.
Single player specific:
@ -235,4 +231,3 @@ Only during development:
- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true)
- [https://m.eaglercraft.com/](EaglerCraft) - Eaglercraft runnable on mobile (real Minecraft in the browser)
- [js-minecraft](https://github.com/LabyStudio/js-minecraft) - An insanely well done clone from the graphical side that inspired many features here

View file

@ -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.webmc.fun",
"name": "WebMC"
},
{
"ip": "wss://ws.fuchsmc.net"
},

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

@ -1,4 +1,8 @@
import * as THREE from 'three'
import globalTexture from 'mc-assets/dist/blocksAtlasLegacy.png'
// Import the renderBlockThree function
import { renderBlockThree } from '../renderer/viewer/lib/mesher/standaloneRenderer'
// Create scene, camera and renderer
const scene = new THREE.Scene()
@ -8,53 +12,292 @@ renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// Position camera
camera.position.z = 5
camera.position.set(3, 3, 3)
camera.lookAt(0, 0, 0)
// Create a canvas with some content
const canvas = document.createElement('canvas')
canvas.width = 256
canvas.height = 256
const ctx = canvas.getContext('2d')
// Dark background
scene.background = new THREE.Color(0x333333)
scene.background = new THREE.Color(0x444444)
// Add some lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4)
directionalLight.position.set(1, 1, 1)
scene.add(directionalLight)
// Draw something on the canvas
ctx.fillStyle = '#444444'
// ctx.fillRect(0, 0, 256, 256)
ctx.fillStyle = 'red'
ctx.font = '48px Arial'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('Hello!', 128, 128)
// Add grid helper for orientation
const gridHelper = new THREE.GridHelper(10, 10)
scene.add(gridHelper)
// Create bitmap and texture
async function createTexturedBox() {
const canvas2 = new OffscreenCanvas(256, 256)
const ctx2 = canvas2.getContext('2d')!
ctx2.drawImage(canvas, 0, 0)
const texture = new THREE.Texture(canvas2)
// Create shared material that will be used by all blocks
const sharedMaterial = new THREE.MeshLambertMaterial({
vertexColors: true,
transparent: true,
alphaTest: 0.1,
// wireframe: true // Add wireframe for debugging
})
// Create simple block models for testing
function createFullBlockModel(textureObj: any): any {
return [[{
elements: [{
from: [0, 0, 0],
to: [16, 16, 16],
faces: {
up: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
down: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
north: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
south: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
east: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
west: {
texture: textureObj,
uv: [0, 0, 16, 16]
}
}
}]
}]]
}
function createHalfBlockModel(textureObj: any): any {
return [[{
elements: [{
from: [0, 0, 0],
to: [16, 8, 16], // Half height (8 instead of 16)
faces: {
up: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
down: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
north: {
texture: textureObj,
uv: [0, 0, 16, 8] // Half height UV
},
south: {
texture: textureObj,
uv: [0, 0, 16, 8] // Half height UV
},
east: {
texture: textureObj,
uv: [0, 0, 16, 8] // Half height UV
},
west: {
texture: textureObj,
uv: [0, 0, 16, 8] // Half height UV
}
}
}]
}]]
}
let currentFullBlockInstancedMesh: THREE.InstancedMesh | null = null
let currentHalfBlockInstancedMesh: THREE.InstancedMesh | null = null
async function createInstancedBlock() {
try {
// Clean up previous meshes if they exist
if (currentFullBlockInstancedMesh) {
scene.remove(currentFullBlockInstancedMesh)
currentFullBlockInstancedMesh.geometry.dispose()
}
if (currentHalfBlockInstancedMesh) {
scene.remove(currentHalfBlockInstancedMesh)
currentHalfBlockInstancedMesh.geometry.dispose()
}
// Load the blocks atlas texture
const textureLoader = new THREE.TextureLoader()
const texture = await textureLoader.loadAsync(globalTexture)
// Configure texture for pixel art
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
texture.needsUpdate = true
texture.generateMipmaps = false
texture.flipY = false
// Create box with texture
const geometry = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
premultipliedAlpha: false,
})
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
// Set the texture on our shared material
sharedMaterial.map = texture
sharedMaterial.needsUpdate = true
console.log('Texture loaded:', texture.image.width, 'x', texture.image.height)
// Calculate UV coordinates for the first tile (top-left, 16x16)
const atlasWidth = texture.image.width
const atlasHeight = texture.image.height
const tileSize = 16
const textureInfo = {
u: 0 / atlasWidth, // Left edge (first column)
v: 2 * tileSize / atlasHeight, // Top edge (third row)
su: tileSize / atlasWidth, // Width of one tile
sv: tileSize / atlasHeight // Height of one tile
}
console.log('Atlas size:', atlasWidth, 'x', atlasHeight)
console.log('Calculated texture info:', textureInfo)
// Create mock texture object that matches what the renderer expects
const mockTexture = {
u: textureInfo.u,
v: textureInfo.v,
su: textureInfo.su,
sv: textureInfo.sv,
debugName: 'test_texture'
}
// Create block models with the mock texture
const fullBlockModel = createFullBlockModel(mockTexture)
const halfBlockModel = createHalfBlockModel(mockTexture)
// Mock data for the renderBlockThree function
const mockBlock = undefined // No specific block data needed for this test
const mockBiome = 'plains'
const mockMcData = {} as any
const mockVariants = []
const mockNeighbors = {}
// Render the full block
const fullBlockGeometry = renderBlockThree(
fullBlockModel,
mockBlock,
mockBiome,
mockMcData,
mockVariants,
mockNeighbors
)
// Render the half block
const halfBlockGeometry = renderBlockThree(
halfBlockModel,
mockBlock,
mockBiome,
mockMcData,
mockVariants,
mockNeighbors
)
// Create instanced mesh for full blocks
currentFullBlockInstancedMesh = new THREE.InstancedMesh(fullBlockGeometry, sharedMaterial, 2) // Support 2 instances
const matrix = new THREE.Matrix4()
// First instance (full block)
matrix.setPosition(-1.5, 0.5, 0.5)
currentFullBlockInstancedMesh.setMatrixAt(0, matrix)
// Second instance (full block)
matrix.setPosition(1.5, 0.5, 0.5)
currentFullBlockInstancedMesh.setMatrixAt(1, matrix)
currentFullBlockInstancedMesh.count = 2
currentFullBlockInstancedMesh.instanceMatrix.needsUpdate = true
scene.add(currentFullBlockInstancedMesh)
// Create instanced mesh for half blocks
currentHalfBlockInstancedMesh = new THREE.InstancedMesh(halfBlockGeometry, sharedMaterial, 1) // Support 1 instance
const halfMatrix = new THREE.Matrix4()
// Half block instance
halfMatrix.setPosition(0, 0.75, 0.5) // Positioned higher so top aligns with full blocks
currentHalfBlockInstancedMesh.setMatrixAt(0, halfMatrix)
currentHalfBlockInstancedMesh.count = 1
currentHalfBlockInstancedMesh.instanceMatrix.needsUpdate = true
scene.add(currentHalfBlockInstancedMesh)
console.log('Instanced blocks created successfully')
console.log('Full block geometry:', fullBlockGeometry)
console.log('Half block geometry:', halfBlockGeometry)
} catch (error) {
console.error('Error creating instanced blocks:', error)
// Fallback: create colored cubes
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshLambertMaterial({ color: 0xff0000, wireframe: true })
const fallbackMesh = new THREE.Mesh(geometry, material)
fallbackMesh.position.set(0, 0.5, 0.5)
scene.add(fallbackMesh)
console.log('Created fallback colored cube')
}
}
// Create the textured box
createTexturedBox()
// Create the instanced block
createInstancedBlock().then(() => {
render()
})
// Animation loop
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
// Simple render loop (no animation)
function render() {
renderer.render(scene, camera)
}
animate()
// Add mouse controls for better viewing
let mouseDown = false
let mouseX = 0
let mouseY = 0
renderer.domElement.addEventListener('mousedown', (event) => {
mouseDown = true
mouseX = event.clientX
mouseY = event.clientY
})
renderer.domElement.addEventListener('mousemove', (event) => {
if (!mouseDown) return
const deltaX = event.clientX - mouseX
const deltaY = event.clientY - mouseY
// Rotate camera around the center
const spherical = new THREE.Spherical()
spherical.setFromVector3(camera.position)
spherical.theta -= deltaX * 0.01
spherical.phi += deltaY * 0.01
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi))
camera.position.setFromSpherical(spherical)
camera.lookAt(0, 0, 0)
mouseX = event.clientX
mouseY = event.clientY
render()
})
renderer.domElement.addEventListener('mouseup', () => {
mouseDown = false
})
// Add button to recreate blocks (for testing)
const button = document.createElement('button')
button.textContent = 'Recreate Blocks'
button.style.position = 'fixed'
button.style.top = '10px'
button.style.left = '10px'
button.addEventListener('click', () => {
createInstancedBlock()
render()
})
document.body.appendChild(button)
// Initial render
render()

View file

@ -54,7 +54,6 @@
"dependencies": {
"@dimaka/interface": "0.0.3-alpha.0",
"@floating-ui/react": "^0.26.1",
"@monaco-editor/react": "^4.7.0",
"@nxg-org/mineflayer-auto-jump": "^0.7.18",
"@nxg-org/mineflayer-tracker": "1.3.0",
"@react-oauth/google": "^0.12.1",
@ -80,14 +79,14 @@
"esbuild-plugin-polyfill-node": "^0.3.0",
"express": "^4.18.2",
"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",
"fs-extra": "^11.1.1",
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"mcraft-fun-mineflayer": "^0.1.23",
"minecraft-data": "3.98.0",
"minecraft-data": "3.92.0",
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
"mojangson": "^2.0.4",
@ -157,7 +156,8 @@
"mc-assets": "^0.2.62",
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
"mineflayer-mouse": "^0.1.21",
"mineflayer-mouse": "^0.1.14",
"mineflayer-pathfinder": "^2.4.4",
"npm-run-all": "^4.1.5",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
@ -197,7 +197,6 @@
},
"pnpm": {
"overrides": {
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
"@nxg-org/mineflayer-physics-util": "1.8.10",
"buffer": "^6.0.3",
"vec3": "0.1.10",
@ -205,7 +204,7 @@
"diamond-square": "github:zardoy/diamond-square",
"prismarine-block": "github:zardoy/prismarine-block#next-era",
"prismarine-world": "github:zardoy/prismarine-world#next-era",
"minecraft-data": "3.98.0",
"minecraft-data": "3.92.0",
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
"prismarine-physics": "github:zardoy/prismarine-physics",
"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
index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b43cb102a 100644
index a50f4b988ad9fb29d5eb9e1633b498615aa9cd28..b8b819eef0762a77d9db5c0fb06be648303628c7 100644
--- a/src/client/chat.js
+++ b/src/client/chat.js
@@ -116,7 +116,7 @@ module.exports = function (client, options) {
@@ -110,7 +110,7 @@ module.exports = function (client, options) {
for (const player of packet.data) {
if (player.chatSession) {
client._players[player.uuid] = {
@ -11,8 +11,8 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
publicKeyDER: player.chatSession.publicKey.keyBytes,
sessionUuid: player.chatSession.uuid
}
@@ -126,7 +126,7 @@ module.exports = function (client, options) {
@@ -120,7 +120,7 @@ module.exports = function (client, options) {
if (player.crypto) {
client._players[player.uuid] = {
- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
@ -20,7 +20,7 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
publicKeyDER: player.crypto.publicKey,
signature: player.crypto.signature,
displayName: player.displayName || player.name
@@ -196,7 +196,7 @@ module.exports = function (client, options) {
@@ -190,7 +190,7 @@ module.exports = function (client, options) {
if (mcData.supportFeature('useChatSessions')) {
const tsDelta = BigInt(Date.now()) - packet.timestamp
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
@ -29,16 +29,16 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
if (verified) client._signatureCache.push(packet.signature)
client.emit('playerChat', {
globalIndex: packet.globalIndex,
@@ -362,7 +362,7 @@ module.exports = function (client, options) {
@@ -356,7 +356,7 @@ module.exports = function (client, options) {
}
}
- client._signedChat = (message, options = {}) => {
+ client._signedChat = async (message, options = {}) => {
options.timestamp = options.timestamp || BigInt(Date.now())
options.salt = options.salt || 1n
@@ -407,7 +407,7 @@ module.exports = function (client, options) {
@@ -401,7 +401,7 @@ module.exports = function (client, options) {
message,
timestamp: options.timestamp,
salt: options.salt,
@ -47,7 +47,7 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
offset: client._lastSeenMessages.pending,
checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+
acknowledged
@@ -422,7 +422,7 @@ module.exports = function (client, options) {
@@ -416,7 +416,7 @@ module.exports = function (client, options) {
message,
timestamp: options.timestamp,
salt: options.salt,
@ -71,23 +71,10 @@ index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f8
+ // clearTimeout(loginTimeout)
+ // })
}
function onJoinServerResponse (err) {
diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js
index 671eb452f31e6b5fcd57d715f1009d010160c65f..7f69f511c8fb97d431ec5125c851b49be8e2ab76 100644
--- a/src/client/pluginChannels.js
+++ b/src/client/pluginChannels.js
@@ -57,7 +57,7 @@ module.exports = function (client, options) {
try {
packet.data = proto.parsePacketBuffer(channel, packet.data).data
} catch (error) {
- client.emit('error', error)
+ client.emit('error', error, { customPayload: packet })
return
}
}
diff --git a/src/client.js b/src/client.js
index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875989cdf0c 100644
index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..11c6bff299f1186ab1ecb6744f53ff0c648ab192 100644
--- a/src/client.js
+++ b/src/client.js
@@ -111,7 +111,13 @@ class Client extends EventEmitter {
@ -107,7 +94,7 @@ index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875
}
@@ -169,7 +175,10 @@ class Client extends EventEmitter {
}
const onFatalError = (err) => {
- this.emit('error', err)
+ // todo find out what is trying to write after client disconnect
@ -116,23 +103,58 @@ index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875
+ }
endSocket()
}
@@ -198,6 +207,10 @@ class Client extends EventEmitter {
@@ -198,6 +207,8 @@ class Client extends EventEmitter {
serializer -> framer -> socket -> splitter -> deserializer */
if (this.serializer) {
this.serializer.end()
+ setTimeout(() => {
+ this.socket?.end()
+ this.socket?.emit('end')
+ }, 2000) // allow the serializer to finish writing
+ this.socket?.end()
+ this.socket?.emit('end')
} else {
if (this.socket) this.socket.end()
}
@@ -243,6 +256,7 @@ class Client extends EventEmitter {
@@ -243,6 +254,7 @@ class Client extends EventEmitter {
debug('writing packet ' + this.state + '.' + name)
debug(params)
}
+ this.emit('writePacket', name, params)
this.serializer.write({ name, params })
}
diff --git a/src/client.js.rej b/src/client.js.rej
new file mode 100644
index 0000000000000000000000000000000000000000..1101e2477adfdc004381b78e7d70953dacb7b484
--- /dev/null
+++ b/src/client.js.rej
@@ -0,0 +1,31 @@
+@@ -89,10 +89,12 @@
+ 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
+@@ -239,8 +252,11 @@
+
+ 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.serializer.write({ name, params })
+ }
+

242
pnpm-lock.yaml generated
View file

@ -5,7 +5,6 @@ settings:
excludeLinksFromLockfile: false
overrides:
mineflayer: github:zardoy/mineflayer#gen-the-master
'@nxg-org/mineflayer-physics-util': 1.8.10
buffer: ^6.0.3
vec3: 0.1.10
@ -13,7 +12,7 @@ overrides:
diamond-square: github:zardoy/diamond-square
prismarine-block: github:zardoy/prismarine-block#next-era
prismarine-world: github:zardoy/prismarine-world#next-era
minecraft-data: 3.98.0
minecraft-data: 3.92.0
prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything
prismarine-physics: github:zardoy/prismarine-physics
minecraft-protocol: github:PrismarineJS/node-minecraft-protocol#master
@ -23,7 +22,7 @@ overrides:
patchedDependencies:
minecraft-protocol:
hash: 4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b
hash: b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9
path: patches/minecraft-protocol.patch
mineflayer-item-map-downloader@1.2.0:
hash: a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad
@ -42,9 +41,6 @@ importers:
'@floating-ui/react':
specifier: ^0.26.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':
specifier: ^0.7.18
version: 0.7.18
@ -121,8 +117,8 @@ importers:
specifier: ^10.0.12
version: 10.1.6
flying-squid:
specifier: npm:@zardoy/flying-squid@^0.0.104
version: '@zardoy/flying-squid@0.0.104(encoding@0.1.13)'
specifier: npm:@zardoy/flying-squid@^0.0.62
version: '@zardoy/flying-squid@0.0.62(encoding@0.1.13)'
framer-motion:
specifier: ^12.9.2
version: 12.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -140,13 +136,13 @@ importers:
version: 4.17.21
mcraft-fun-mineflayer:
specifier: ^0.1.23
version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13))
version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13))
minecraft-data:
specifier: 3.98.0
version: 3.98.0
specifier: 3.92.0
version: 3.92.0
minecraft-protocol:
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/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13)
mineflayer-item-map-downloader:
specifier: github:zardoy/mineflayer-item-map-downloader
version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13)
@ -155,7 +151,7 @@ importers:
version: 2.0.4
net-browserify:
specifier: github:zardoy/prismarinejs-net-browserify
version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618
version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/17fb901e8ea480a52c8fd46373695be172be8aa5
node-gzip:
specifier: ^1.1.2
version: 1.1.2
@ -170,7 +166,7 @@ importers:
version: 6.1.1
prismarine-provider-anvil:
specifier: github:zardoy/prismarine-provider-anvil#everything
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0)
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
prosemirror-example-setup:
specifier: ^1.2.2
version: 1.2.3
@ -345,10 +341,13 @@ importers:
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1)
mineflayer:
specifier: github:zardoy/mineflayer#gen-the-master
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13)
mineflayer-mouse:
specifier: ^0.1.21
version: 0.1.21
specifier: ^0.1.14
version: 0.1.14
mineflayer-pathfinder:
specifier: ^2.4.4
version: 2.4.5
npm-run-all:
specifier: ^4.1.5
version: 4.1.5
@ -436,7 +435,7 @@ importers:
version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-chunk:
specifier: github:zardoy/prismarine-chunk#master
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
prismarine-schematic:
specifier: ^1.2.0
version: 1.2.3
@ -1992,16 +1991,6 @@ packages:
'@module-federation/webpack-bundler-runtime@0.11.2':
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':
resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==}
engines: {node: '>= 10'}
@ -3387,13 +3376,13 @@ packages:
resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==}
engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'}
'@zardoy/flying-squid@0.0.104':
resolution: {integrity: sha512-jGhQ7fn7o8UN+mUwZbt9674D37YLuBi+Au4TwKcopCA6huIQdHTFNl2e+0ZSTI5mnhN+NpyVoR3vmtH6L58vHQ==}
'@zardoy/flying-squid@0.0.49':
resolution: {integrity: sha512-Kt4wr5/R+44tcLU9gjuNG2an9weWeKEpIoKXfsgJN2GGQqdnbd5nBpxfGDdgZ9aMdFugsVW8BsyPZNhj9vbMXA==}
engines: {node: '>=8'}
hasBin: true
'@zardoy/flying-squid@0.0.49':
resolution: {integrity: sha512-Kt4wr5/R+44tcLU9gjuNG2an9weWeKEpIoKXfsgJN2GGQqdnbd5nBpxfGDdgZ9aMdFugsVW8BsyPZNhj9vbMXA==}
'@zardoy/flying-squid@0.0.62':
resolution: {integrity: sha512-M6icydO/yrmwevBhmgKcqEPC63AhWfU/Es9N/uadVrmKaxGm2FQMMLcybbutRYm1xZ6qsdxDUOUZnN56PsVwfQ==}
engines: {node: '>=8'}
hasBin: true
@ -6444,12 +6433,6 @@ packages:
resolution: {integrity: sha512-RYZeD1+joNlPuUpi+tIWkbP0ieVJr+R6IFkI6/8juhSxx9zE4osoSmteybrfspGm8A6u+YbbY1epqRKEMwVR6Q==}
engines: {node: '>=18.0.0'}
mc-bridge@0.1.3:
resolution: {integrity: sha512-H9jPt2xEU77itC27dSz3qazHYqN9qVsv4HgMPozg7RqQ1uwgXmEa+ojKIlDtXf/TLJsG6Kv4EbzGa8a1Wh72uA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
minecraft-data: 3.98.0
mcraft-fun-mineflayer@0.1.23:
resolution: {integrity: sha512-qmI1rQQ0Ro5zJdi99rClWLF+mS9JZffgNX2vyWWesS3Hsk3Xblp/8swYTJKHSaFpNgzkVfXV92fEIrBqeH6iKA==}
version: 0.1.23
@ -6658,8 +6641,8 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
minecraft-data@3.98.0:
resolution: {integrity: sha512-JAPqJ/TZoxMUlAPPdWUh1v5wdqvYGFSZ4rW9bUtmaKBkGpomDSjw4V02ocBqbxKJvcTtmc5nM/LfN9/0DDqHrQ==}
minecraft-data@3.92.0:
resolution: {integrity: sha512-CGfO50svzm+pSRa4Mbq4owsmRKbPCNkSZ3MCOyH+epC7yNjh+PUhPQFHWq72O51qsY7pAB5qM/bJn1ncwG1J5g==}
minecraft-folder-path@1.2.0:
resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==}
@ -6668,9 +6651,9 @@ packages:
resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41}
version: 1.0.1
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9:
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9}
version: 1.62.0
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb:
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb}
version: 1.60.1
engines: {node: '>=22'}
minecraft-wrap@1.6.0:
@ -6684,13 +6667,20 @@ packages:
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824}
version: 1.2.0
mineflayer-mouse@0.1.21:
resolution: {integrity: sha512-1XTVuw3twIrEcqQ1QRSB8NcStIUEZ+tbxiAG6rOrN/9M4thhtlS5PTJzFdmdrcYgWEBLvuOdJszaKE5zFfiXhg==}
mineflayer-mouse@0.1.14:
resolution: {integrity: sha512-DjytRMlRLxR44GqZ6udMgbMO4At7Ura5TQC80exRhzkfptyCGLTWzXaf0oeXSNYkNMnaaEv4XP/9YRwuvL+rsQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659:
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659}
version: 8.0.0
mineflayer-pathfinder@2.4.5:
resolution: {integrity: sha512-Jh3JnUgRLwhMh2Dugo4SPza68C41y+NPP5sdsgxRu35ydndo70i1JJGxauVWbXrpNwIxYNztUw78aFyb7icw8g==}
mineflayer@4.31.0:
resolution: {integrity: sha512-oqiNa5uP4kXiPlj4+Jn+9QozPMsMy0U8/YP5d6+KSAeWthtuJHeQqcYgWG5lkC3LHMqHqtEu4MNdXt6GZjFNTQ==}
engines: {node: '>=22'}
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448:
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448}
version: 4.31.0
engines: {node: '>=22'}
minimalistic-assert@1.0.1:
@ -6788,9 +6778,6 @@ packages:
mojangson@2.0.4:
resolution: {integrity: sha512-HYmhgDjr1gzF7trGgvcC/huIg2L8FsVbi/KacRe6r1AswbboGVZDS47SOZlomPuMWvZLas8m9vuHHucdZMwTmQ==}
monaco-editor@0.52.2:
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
moo@0.5.2:
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
@ -6855,8 +6842,8 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/e754999ffdea67853bc9b10553b5e9908b40f618:
resolution: {tarball: 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/17fb901e8ea480a52c8fd46373695be172be8aa5}
version: 0.2.4
nice-try@1.0.5:
@ -7387,7 +7374,7 @@ packages:
prismarine-biome@1.3.0:
resolution: {integrity: sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==}
peerDependencies:
minecraft-data: 3.98.0
minecraft-data: 3.92.0
prismarine-registry: ^1.1.0
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9:
@ -8395,9 +8382,6 @@ packages:
stacktrace-js@2.0.2:
resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==}
state-local@1.0.7:
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
static-extend@0.1.2:
resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
engines: {node: '>=0.10.0'}
@ -10310,7 +10294,7 @@ snapshots:
'@babel/parser': 7.26.9
'@babel/template': 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
transitivePeerDependencies:
- supports-color
@ -11279,17 +11263,6 @@ snapshots:
'@module-federation/runtime': 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': {}
'@ndelangen/get-tarball@3.0.9':
@ -11343,8 +11316,8 @@ snapshots:
'@nxg-org/mineflayer-trajectories@1.2.0(encoding@0.1.13)':
dependencies:
'@nxg-org/mineflayer-util-plugin': 1.8.4
minecraft-data: 3.98.0
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
minecraft-data: 3.92.0
mineflayer: 4.31.0(encoding@0.1.13)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-item: 1.17.0
prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b
@ -12891,7 +12864,7 @@ snapshots:
dependencies:
'@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)
debug: 4.4.1
debug: 4.4.0(supports-color@8.1.1)
eslint: 8.57.1
ts-api-utils: 1.4.3(typescript@5.5.4)
optionalDependencies:
@ -12923,7 +12896,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 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
is-glob: 4.0.3
minimatch: 9.0.3
@ -12938,7 +12911,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 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
is-glob: 4.0.3
minimatch: 9.0.5
@ -13094,7 +13067,7 @@ snapshots:
'@types/emscripten': 1.40.0
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:
'@tootallnate/once': 2.0.0
chalk: 5.4.1
@ -13104,18 +13077,16 @@ snapshots:
exit-hook: 2.2.1
flatmap: 0.0.3
long: 5.3.1
mc-bridge: 0.1.3(minecraft-data@3.98.0)
minecraft-data: 3.98.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
minecraft-data: 3.92.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13)
mkdirp: 2.1.6
node-gzip: 1.1.2
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.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-item: 1.17.0
prismarine-nbt: 2.7.0
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.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-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
rambda: 9.4.2
@ -13132,7 +13103,7 @@ snapshots:
- encoding
- supports-color
'@zardoy/flying-squid@0.0.49(encoding@0.1.13)':
'@zardoy/flying-squid@0.0.62(encoding@0.1.13)':
dependencies:
'@tootallnate/once': 2.0.0
chalk: 5.4.1
@ -13142,16 +13113,16 @@ snapshots:
exit-hook: 2.2.1
flatmap: 0.0.3
long: 5.3.1
minecraft-data: 3.98.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
minecraft-data: 3.92.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13)
mkdirp: 2.1.6
node-gzip: 1.1.2
node-rsa: 1.1.1
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.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-item: 1.17.0
prismarine-nbt: 2.7.0
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.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-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
rambda: 9.4.2
@ -14542,8 +14513,8 @@ snapshots:
diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71:
dependencies:
minecraft-data: 3.98.0
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(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.92.0)
prismarine-registry: 1.11.0
random-seed: 0.3.0
vec3: 0.1.10
@ -16986,16 +16957,12 @@ snapshots:
maxrects-packer: '@zardoy/maxrects-packer@2.7.4'
zod: 3.24.2
mc-bridge@0.1.3(minecraft-data@3.98.0):
dependencies:
minecraft-data: 3.98.0
mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)):
mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13)):
dependencies:
'@zardoy/flying-squid': 0.0.49(encoding@0.1.13)
exit-hook: 2.2.1
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13)
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/c9c77d6511e37c452ebe48790724da165d6ad448(encoding@0.1.13)
prismarine-item: 1.17.0
ws: 8.18.1
transitivePeerDependencies:
@ -17302,7 +17269,7 @@ snapshots:
min-indent@1.0.1: {}
minecraft-data@3.98.0: {}
minecraft-data@3.92.0: {}
minecraft-folder-path@1.2.0: {}
@ -17313,7 +17280,7 @@ snapshots:
- '@types/react'
- react
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/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(encoding@0.1.13):
dependencies:
'@types/node-rsa': 1.1.4
'@types/readable-stream': 4.0.18
@ -17322,7 +17289,7 @@ snapshots:
debug: 4.4.0(supports-color@8.1.1)
endian-toggle: 0.0.0
lodash.merge: 4.6.2
minecraft-data: 3.98.0
minecraft-data: 3.92.0
minecraft-folder-path: 1.2.0
node-fetch: 2.7.0(encoding@0.1.13)
node-rsa: 0.4.2
@ -17365,13 +17332,13 @@ snapshots:
mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13):
dependencies:
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
mineflayer: 4.31.0(encoding@0.1.13)
sharp: 0.30.7
transitivePeerDependencies:
- encoding
- supports-color
mineflayer-mouse@0.1.21:
mineflayer-mouse@0.1.14:
dependencies:
change-case: 5.4.4
debug: 4.4.1
@ -17380,15 +17347,48 @@ snapshots:
transitivePeerDependencies:
- supports-color
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13):
mineflayer-pathfinder@2.4.5:
dependencies:
'@nxg-org/mineflayer-physics-util': 1.8.10
minecraft-data: 3.98.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0)
minecraft-data: 3.92.0
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-entity: 2.5.0
prismarine-item: 1.17.0
prismarine-nbt: 2.7.0
prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b
vec3: 0.1.10
mineflayer@4.31.0(encoding@0.1.13):
dependencies:
minecraft-data: 3.92.0
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(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.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-item: 1.17.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/c9c77d6511e37c452ebe48790724da165d6ad448(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/b404bcaed4c039c5558e889c8617aa866cd7bddb(patch_hash=b417b3b7c5fd96e59abab5c1075b86b88bada2c980e4b54df13ca69b8f0091d9)(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.17.0
prismarine-nbt: 2.7.0
@ -17503,8 +17503,6 @@ snapshots:
dependencies:
nearley: 2.20.1
monaco-editor@0.52.2: {}
moo@0.5.2: {}
morgan@1.10.0:
@ -17586,7 +17584,7 @@ snapshots:
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:
body-parser: 1.20.3
express: 4.21.2
@ -18174,15 +18172,15 @@ snapshots:
transitivePeerDependencies:
- 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:
minecraft-data: 3.98.0
minecraft-data: 3.92.0
prismarine-registry: 1.11.0
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9:
dependencies:
minecraft-data: 3.98.0
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0)
minecraft-data: 3.92.0
prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
prismarine-chat: 1.11.0
prismarine-item: 1.17.0
prismarine-nbt: 2.7.0
@ -18194,9 +18192,9 @@ snapshots:
prismarine-nbt: 2.7.0
prismarine-registry: 1.11.0
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0):
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0):
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-nbt: 2.7.0
prismarine-registry: 1.11.0
@ -18225,14 +18223,14 @@ snapshots:
prismarine-physics@https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b:
dependencies:
minecraft-data: 3.98.0
minecraft-data: 3.92.0
prismarine-nbt: 2.7.0
vec3: 0.1.10
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0):
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0):
dependencies:
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.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-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
uint4: 0.1.2
@ -18254,13 +18252,13 @@ snapshots:
prismarine-registry@1.11.0:
dependencies:
minecraft-data: 3.98.0
minecraft-data: 3.92.0
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-nbt: 2.7.0
prismarine-schematic@1.2.3:
dependencies:
minecraft-data: 3.98.0
minecraft-data: 3.92.0
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
prismarine-nbt: 2.7.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
@ -18459,7 +18457,7 @@ snapshots:
puppeteer-core@2.1.1:
dependencies:
'@types/mime-types': 2.1.4
debug: 4.4.1
debug: 4.4.0(supports-color@8.1.1)
extract-zip: 1.7.0
https-proxy-agent: 4.0.0
mime: 2.6.0
@ -19561,8 +19559,6 @@ snapshots:
stack-generator: 2.0.10
stacktrace-gps: 3.1.2
state-local@1.0.7: {}
static-extend@0.1.2:
dependencies:
define-property: 0.2.5

View file

@ -65,7 +65,7 @@ function getAllMethods (obj) {
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
for (let i = 0; i < arr.length; i += chunkSize) {
if (delay) {
@ -74,6 +74,6 @@ export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item:
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) => {
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data|prismarine-physics/, (resource) => {
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data/, (resource) => {
let absolute: string
const request = resource.request.replaceAll('\\', '/')
absolute = path.join(resource.context, request).replaceAll('\\', '/')
if (request.includes('minecraft-data/data/pc/1.') || request.includes('prismarine-physics')) {
console.log('Error: incompatible resource', request, 'from', resource.contextInfo.issuer)
if (request.includes('minecraft-data/data/pc/1.')) {
console.log('Error: incompatible resource', request, resource.contextInfo.issuer)
process.exit(1)
// 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,
heldItemOff: undefined as HandItemBlock | undefined,
perspective: 'first_person' as CameraPerspective,
onFire: false,
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,7 @@
import { WorldBlock as Block, World } from './world'
export const isBlockInstanceable = (world: World, block: Block): boolean => {
const instancedBlocks = world?.instancedBlocks
if (!instancedBlocks) return false
return instancedBlocks.includes(block.stateId)
}

View file

@ -1,7 +1,7 @@
import { Vec3 } from 'vec3'
import { World } from './world'
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
import { BlockStateModelInfo } from './shared'
import { BlockStateModelInfo, InstancingMode } from './shared'
import { INVISIBLE_BLOCKS } from './worldConstants'
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
@ -17,7 +17,7 @@ if (module.require) {
let workerIndex = 0
let world: World
let dirtySections = new Map<string, number>()
let dirtySections = new Map<string, { key: string, instancingMode: InstancingMode, times: number }>()
let allDataReady = false
function sectionKey (x, y, z) {
@ -47,7 +47,7 @@ function drainQueue (from, to) {
queuedMessages = queuedMessages.slice(to)
}
function setSectionDirty (pos, value = true) {
function setSectionDirty (pos, value = true, instancingMode = InstancingMode.None) {
const x = Math.floor(pos.x / 16) * 16
const y = Math.floor(pos.y / 16) * 16
const z = Math.floor(pos.z / 16) * 16
@ -60,7 +60,7 @@ function setSectionDirty (pos, value = true) {
const chunk = world.getColumn(x, z)
if (chunk?.getSection(pos)) {
dirtySections.set(key, (dirtySections.get(key) || 0) + 1)
dirtySections.set(key, { key, instancingMode, times: (dirtySections.get(key)?.times || 0) + 1 })
} else {
postMessage({ type: 'sectionFinished', key, workerIndex })
}
@ -68,8 +68,7 @@ function setSectionDirty (pos, value = true) {
const softCleanup = () => {
// clean block cache and loaded chunks
world = new World(world.config.version)
globalThis.world = world
world.blockCache = {}
}
const handleMessage = data => {
@ -98,12 +97,14 @@ const handleMessage = data => {
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
allDataReady = true
workerIndex = data.workerIndex
world.instancedBlocks = data.instancedBlocks
world.instancedBlockIds = data.instancedBlockIds || {}
break
}
case 'dirty': {
const loc = new Vec3(data.x, data.y, data.z)
setSectionDirty(loc, data.value)
setSectionDirty(loc, data.value, data.instancingMode || InstancingMode.None)
break
}
@ -190,6 +191,18 @@ self.onmessage = ({ data }) => {
handleMessage(data)
}
// Debug flag to spam last geometry output
globalThis.DEBUG_GEOMETRY_SPAM = false // set to true to enable geometry spam for performance testing
globalThis.lastGeometryKey = null
// Track last 50 unique geometry objects with their respective keys for aggressive debugging
interface GeometryEntry {
key: string
geometry: any
}
const lastGeometryEntries: GeometryEntry[] = []
const MAX_GEOMETRY_ENTRIES = 50
setInterval(() => {
if (world === null || !allDataReady) return
@ -197,23 +210,37 @@ setInterval(() => {
// console.log(sections.length + ' dirty sections')
// const start = performance.now()
for (const key of dirtySections.keys()) {
for (const [key, { instancingMode }] of dirtySections.entries()) {
const [x, y, z] = key.split(',').map(v => parseInt(v, 10))
const chunk = world.getColumn(x, z)
let processTime = 0
if (chunk?.getSection(new Vec3(x, y, z))) {
const start = performance.now()
const geometry = getSectionGeometry(x, y, z, world)
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean)
//@ts-expect-error
postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable)
const geometry = getSectionGeometry(x, y, z, world, instancingMode)
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean) as any
postMessage({ type: 'geometry', key, geometry, workerIndex }/* , transferable */)
processTime = performance.now() - start
// Store last geometry for debug spam
globalThis.lastGeometryKey = key
// Track unique geometry entries for aggressive debugging
const existingIndex = lastGeometryEntries.findIndex(entry => entry.key === key)
if (existingIndex >= 0) {
// Update existing entry with new geometry
lastGeometryEntries[existingIndex].geometry = geometry
} else {
// Add new entry
lastGeometryEntries.push({ key, geometry })
if (lastGeometryEntries.length > MAX_GEOMETRY_ENTRIES) {
lastGeometryEntries.shift() // Remove oldest
}
}
} else {
// console.info('[mesher] Missing section', x, y, z)
}
const dirtyTimes = dirtySections.get(key)
if (!dirtyTimes) throw new Error('dirtySections.get(key) is falsy')
for (let i = 0; i < dirtyTimes; i++) {
for (let i = 0; i < dirtyTimes.times; i++) {
postMessage({ type: 'sectionFinished', key, workerIndex, processTime })
processTime = 0
}
@ -237,3 +264,24 @@ setInterval(() => {
// const time = performance.now() - start
// console.log(`Processed ${sections.length} sections in ${time} ms (${time / sections.length} ms/section)`)
}, 50)
// Debug spam: repeatedly send last geometry output every 100ms
setInterval(() => {
if (globalThis.DEBUG_GEOMETRY_SPAM) {
// Send the last geometry
// Aggressive debugging: send all tracked geometry entries with their respective geometries
// console.log(`[DEBUG] Sending ${lastGeometryEntries.length} unique geometry entries:`, lastGeometryEntries.map(e => e.key))
// Send each unique geometry entry with its respective geometry for maximum stress testing
for (const entry of lastGeometryEntries) {
postMessage({
type: 'geometry',
key: entry.key,
geometry: entry.geometry,
workerIndex,
debug: true // Mark as debug message
})
}
}
}, 20)

View file

@ -5,8 +5,8 @@ import { BlockType } from '../../../playground/shared'
import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock } from './world'
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
import { INVISIBLE_BLOCKS } from './worldConstants'
import { MesherGeometryOutput, HighestBlockInfo } from './shared'
import { MesherGeometryOutput, InstancingMode } from './shared'
import { isBlockInstanceable } from './instancingUtils'
let blockProvider: WorldBlockProvider
@ -132,7 +132,11 @@ const getVec = (v: Vec3, dir: Vec3) => {
return v.plus(dir)
}
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean) {
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean, instancingEnabled: boolean) {
if (instancingEnabled) {
return // todo
}
const heights: number[] = []
for (let z = -1; z <= 1; z++) {
for (let x = -1; x <= 1; x++) {
@ -518,10 +522,64 @@ const isBlockWaterlogged = (block: Block) => {
return block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' || ALWAYS_WATERLOGGED.has(block.name)
}
const shouldCullInstancedBlock = (world: World, cursor: Vec3, block: Block): boolean => {
// Early return for blocks that should never be culled
if (block.transparent) return false
// Check if all 6 faces would be culled (hidden by neighbors)
const cullIfIdentical = block.name.includes('glass') || block.name.includes('ice')
// Cache cursor offset to avoid creating new Vec3 instances
const offsetCursor = new Vec3(0, 0, 0)
// eslint-disable-next-line guard-for-in
for (const face in elemFaces) {
const { dir } = elemFaces[face]
offsetCursor.set(cursor.x + dir[0], cursor.y + dir[1], cursor.z + dir[2])
const neighbor = world.getBlock(offsetCursor, blockProvider, {})
// Face is exposed to air/void - block must be rendered
if (!neighbor) return false
// Handle special case for identical blocks (glass/ice)
if (cullIfIdentical && neighbor.stateId === block.stateId) continue
// If neighbor is not a full opaque cube, face is visible
if (neighbor.transparent || !isCube(neighbor)) return false
}
// All faces are culled, block should not be rendered
return true
}
// Add matrix calculation helper
function calculateInstanceMatrix (pos: { x: number, y: number, z: number }, offset = 0.5): number[] {
// Create a 4x4 matrix array (16 elements)
const matrix = Array.from({ length: 16 }).fill(0) as number[]
// Set identity matrix
matrix[0] = 1 // m11
matrix[5] = 1 // m22
matrix[10] = 1 // m33
matrix[15] = 1 // m44
// Set translation (position)
matrix[12] = pos.x + offset // tx
matrix[13] = pos.y + offset // ty
matrix[14] = pos.z + offset // tz
return matrix
}
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, instancingMode = InstancingMode.None): MesherGeometryOutput {
let delayedRender = [] as Array<() => void>
// Check if instanced rendering is enabled for this section
const enableInstancedRendering = instancingMode !== InstancingMode.None
const forceInstancedOnly = instancingMode === InstancingMode.BlockInstancingOnly || instancingMode === InstancingMode.ColorOnly
const attr: MesherGeometryOutput = {
sx: sx + 8,
sy: sy + 8,
@ -543,7 +601,8 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
signs: {},
// isFull: true,
hadErrors: false,
blocksCount: 0
blocksCount: 0,
instancedBlocks: {}
}
const cursor = new Vec3(0, 0, 0)
@ -606,14 +665,56 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
const pos = cursor.clone()
// eslint-disable-next-line @typescript-eslint/no-loop-func
delayedRender.push(() => {
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr, !isWaterlogged)
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr, !isWaterlogged, forceInstancedOnly)
})
attr.blocksCount++
} else if (block.name === 'lava') {
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, false)
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, false, forceInstancedOnly)
attr.blocksCount++
}
if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
// Check if this block can use instanced rendering
if ((enableInstancedRendering && isBlockInstanceable(world, block))/* || forceInstancedOnly */) {
// Check if block should be culled (all faces hidden by neighbors)
// TODO validate this
if (shouldCullInstancedBlock(world, cursor, block)) {
// Block is completely surrounded, skip rendering
continue
}
const blockKey = block.name
if (!attr.instancedBlocks[blockKey]) {
attr.instancedBlocks[blockKey] = {
stateId: block.stateId,
blockName: block.name,
positions: [],
matrices: [] // Add matrices array
}
}
const pos = {
x: cursor.x,
y: cursor.y,
z: cursor.z
}
// Pre-calculate transformation matrix
const offset = instancingMode === InstancingMode.ColorOnly ? 0 : 0.5
const matrix = calculateInstanceMatrix(pos, offset)
attr.instancedBlocks[blockKey].positions.push(pos)
attr.instancedBlocks[blockKey].matrices.push(matrix)
attr.blocksCount++
continue // Skip regular geometry generation for instanceable blocks
}
// Skip buffer geometry generation if force instanced only mode is enabled
if (forceInstancedOnly) {
// In force instanced only mode, skip all non-instanceable blocks
continue
}
// cache
let { models } = block

View file

@ -1,5 +1,12 @@
import { BlockType } from '../../../playground/shared'
export enum InstancingMode {
None = 'none',
ColorOnly = 'color_only',
BlockInstancing = 'block_instancing',
BlockInstancingOnly = 'block_instancing_only'
}
// only here for easier testing
export const defaultMesherConfig = {
version: '',
@ -12,7 +19,7 @@ export const defaultMesherConfig = {
// textureSize: 1024, // for testing
debugModelVariant: undefined as undefined | number[],
clipWorldBelowY: undefined as undefined | number,
disableSignsMapsSupport: false
disableSignsMapsSupport: false,
}
export type CustomBlockModels = {
@ -21,6 +28,19 @@ export type CustomBlockModels = {
export type MesherConfig = typeof defaultMesherConfig
export interface InstancedBlockEntry {
stateId: number
blockName: string
positions: Array<{ x: number, y: number, z: number }>
matrices: number[][] // Pre-calculated transformation matrices from worker
}
export type InstancingMesherData = {
blocks: {
[stateId: number]: number // instance id
}
}
export type MesherGeometryOutput = {
sx: number,
sy: number,
@ -45,6 +65,8 @@ export type MesherGeometryOutput = {
hadErrors: boolean
blocksCount: number
customBlockModels?: CustomBlockModels
// New instanced blocks data
instancedBlocks: Record<string, InstancedBlockEntry>
}
export interface MesherMainEvents {

View file

@ -5,7 +5,7 @@ import { Vec3 } from 'vec3'
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
import legacyJson from '../../../../src/preflatMap.json'
import { defaultMesherConfig, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey } from './shared'
import { defaultMesherConfig, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig } from './shared'
import { INVISIBLE_BLOCKS } from './worldConstants'
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
@ -42,12 +42,14 @@ export class World {
customBlockModels = new Map<string, CustomBlockModels>() // chunkKey -> blockModels
sentBlockStateModels = new Set<string>()
blockStateModelInfo = new Map<string, BlockStateModelInfo>()
instancedBlocks: number[] = []
instancedBlockIds = {} as Record<number, number>
constructor (version) {
constructor (version: string) {
this.Chunk = Chunks(version) as any
this.biomeCache = mcData(version).biomes
this.preflat = !mcData(version).supportFeature('blockStateId')
this.config.version = version
this.config = { ...defaultMesherConfig, version }
}
getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') {
@ -121,7 +123,6 @@ export class World {
if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
const modelOverride = this.customBlockModels.get(key)?.[blockPosKey]
const column = this.columns[key]
// null column means chunk not loaded
@ -131,6 +132,7 @@ export class World {
const locInChunk = posInChunk(loc)
const stateId = column.getBlockStateId(locInChunk)
const modelOverride = stateId ? this.customBlockModels.get(key)?.[blockPosKey] : undefined
const cacheKey = getBlockAssetsCacheKey(stateId, modelOverride)
if (!this.blockCache[cacheKey]) {

View file

@ -7,7 +7,6 @@ import { Vec3 } from 'vec3'
import { BotEvents } from 'mineflayer'
import { proxy } from 'valtio'
import TypedEmitter from 'typed-emitter'
import { Biome } from 'minecraft-data'
import { delayedIterator } from '../../playground/shared'
import { chunkPos } from './simpleUtils'
@ -21,7 +20,7 @@ export type WorldDataEmitterEvents = {
entityMoved: (data: any) => void
playerEntity: (data: any) => void
time: (data: number) => void
renderDistance: (viewDistance: number) => void
renderDistance: (viewDistance: number, keepChunksDistance: number) => void
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
markAsLoaded: (data: { x: number, z: number }) => void
unloadChunk: (data: { x: number, z: number }) => void
@ -29,8 +28,6 @@ export type WorldDataEmitterEvents = {
updateLight: (data: { pos: Vec3 }) => void
onWorldSwitch: () => void
end: () => void
biomeUpdate: (data: { biome: Biome }) => void
biomeReset: () => void
}
export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
@ -38,15 +35,7 @@ export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmit
}
export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
spiralNumber = 0
gotPanicLastTime = false
panicChunksReload = () => {}
loadedChunks: Record<ChunkPosKey, boolean>
private inLoading = false
private chunkReceiveTimes: number[] = []
private lastChunkReceiveTime = 0
public lastChunkReceiveTimeAvg = 0
private panicTimeout?: NodeJS.Timeout
readonly lastPos: Vec3
private eventListeners: Record<string, any> = {}
private readonly emitter: WorldDataEmitter
@ -90,7 +79,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
updateViewDistance (viewDistance: number) {
this.viewDistance = viewDistance
this.emitter.emit('renderDistance', viewDistance)
this.emitter.emit('renderDistance', viewDistance, this.keepChunksDistance)
}
listenToBot (bot: typeof __type_bot) {
@ -144,19 +133,12 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
this.emitter.emit('entity', { id: e.id, delete: true })
},
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}`]) {
this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`](true)
delete this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`]
} else if (this.loadedChunks[`${pos.x},${pos.z}`]) {
void this.loadChunk(pos, false, 'Received another chunkColumnLoad event while already loaded')
}
this.chunkProgress()
},
chunkColumnUnload: (pos: Vec3) => {
this.unloadChunk(pos)
@ -237,59 +219,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))
this.lastPos.update(pos)
await this._loadChunks(positions, pos)
await this._loadChunks(positions)
}
chunkProgress () {
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
async _loadChunks (positions: Vec3[], sliceSize = 5) {
// stop loading previous chunks
for (const pos of Object.keys(this.waitingSpiralChunksLoad)) {
this.waitingSpiralChunksLoad[pos](false)
delete this.waitingSpiralChunksLoad[pos]
}
const promises = [] as Array<Promise<void>>
let continueLoading = true
this.inLoading = true
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)) {
continueLoading = await new Promise<boolean>(resolve => {
this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`] = resolve
})
}
if (!continueLoading) return
await this.loadChunk(pos, undefined, `spiral ${spiralNumber} from ${centerPos.x},${centerPos.z}`)
this.chunkProgress()
if (!this.world.getColumnAt(pos)) {
continueLoading = await new Promise<boolean>(resolve => {
this.waitingSpiralChunksLoad[`${pos.x},${pos.z}`] = resolve
})
}
if (!continueLoading) return
await this.loadChunk(pos)
})()
promises.push(promise)
})
if (this.panicTimeout) clearTimeout(this.panicTimeout)
this.inLoading = false
this.gotPanicLastTime = false
this.chunkReceiveTimes = []
this.lastChunkReceiveTime = 0
await Promise.all(promises)
}
readdDebug () {
@ -363,37 +319,8 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
delete this.debugChunksInfo[`${pos.x},${pos.z}`]
}
lastBiomeId: number | null = null
udpateBiome (pos: Vec3) {
try {
const biomeId = this.world.getBiome(pos)
if (biomeId !== this.lastBiomeId) {
this.lastBiomeId = biomeId
const biomeData = loadedData.biomes[biomeId]
if (biomeData) {
this.emitter.emit('biomeUpdate', {
biome: biomeData
})
} else {
// unknown biome
this.emitter.emit('biomeReset')
}
}
} catch (e) {
console.error('error updating biome', e)
}
}
lastPosCheck: Vec3 | null = null
async updatePosition (pos: Vec3, force = false) {
if (!this.allowPositionUpdate) return
const posFloored = pos.floored()
if (!force && this.lastPosCheck && this.lastPosCheck.equals(posFloored)) return
this.lastPosCheck = posFloored
this.udpateBiome(pos)
const [lastX, lastZ] = chunkPos(this.lastPos)
const [botX, botZ] = chunkPos(pos)
if (lastX !== botX || lastZ !== botZ || force) {
@ -411,6 +338,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
chunksToUnload.push(p)
}
}
console.log('unloading', chunksToUnload.length, 'total now', Object.keys(this.loadedChunks).length)
for (const p of chunksToUnload) {
this.unloadChunk(p)
}
@ -422,7 +350,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
return undefined!
}).filter(a => !!a)
this.lastPos.update(pos)
void this._loadChunks(positions, pos)
void this._loadChunks(positions)
} else {
this.emitter.emit('chunkPosUpdate', { pos }) // todo-low
this.lastPos.update(pos)

View file

@ -12,7 +12,7 @@ import type { ResourcesManagerTransferred } from '../../../src/resourcesManager'
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
import { SoundSystem } from '../three/threeJsSound'
import { buildCleanupDecorator } from './cleanupDecorator'
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared'
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent, InstancingMode } from './mesher/shared'
import { chunkPos } from './simpleUtils'
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
import { WorldDataEmitterWorker } from './worldDataEmitter'
@ -32,45 +32,40 @@ const toMajorVersion = version => {
export const worldCleanup = buildCleanupDecorator('resetWorld')
export const defaultWorldRendererConfig = {
// Debug settings
showChunkBorders: false,
enableDebugOverlay: false,
// Performance settings
mesherWorkers: 4,
addChunksBatchWaitTime: 200,
_experimentalSmoothChunkLoading: true,
_renderByChunks: false,
// Rendering engine settings
dayCycle: true,
isPlayground: false,
renderEars: true,
skinTexturesProxy: undefined as string | undefined,
// game renderer setting actually
showHand: false,
viewBobbing: false,
extraBlockRenderers: true,
clipWorldBelowY: undefined as number | undefined,
smoothLighting: true,
enableLighting: true,
starfield: true,
defaultSkybox: true,
renderEntities: true,
extraBlockRenderers: true,
foreground: true,
fov: 75,
volume: 1,
// Camera visual related settings
showHand: false,
viewBobbing: false,
renderEars: true,
highlightBlockColor: 'blue',
// Player models
fetchPlayerSkins: true,
skinTexturesProxy: undefined as string | undefined,
// VR settings
addChunksBatchWaitTime: 200,
vrSupport: true,
vrPageGameRendering: true,
// World settings
clipWorldBelowY: undefined as number | undefined,
isPlayground: false
renderEntities: true,
fov: 75,
fetchPlayerSkins: true,
highlightBlockColor: 'blue',
foreground: true,
enableDebugOverlay: false,
_experimentalSmoothChunkLoading: true,
_renderByChunks: false,
volume: 1,
// New instancing options
useInstancedRendering: false,
forceInstancedOnly: false,
dynamicInstancing: false,
dynamicInstancingModeDistance: 1, // chunks beyond this distance use instancing only
dynamicColorModeDistance: 1, // chunks beyond this distance use color mode only
instancedOnlyDistance: 6, // chunks beyond this distance use instancing only
enableSingleColorMode: false, // ultra-performance mode with solid colors
autoLowerRenderDistance: false,
}
export type WorldRendererConfig = typeof defaultWorldRendererConfig
@ -169,6 +164,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
abstract changeBackgroundColor (color: [number, number, number]): void
// Optional method for getting instanced blocks data (implemented by Three.js renderer)
getInstancedBlocksData? (): { instanceableBlocks?: Set<number>, allBlocksStateIdToModelIdMap?: Record<number, number> } | undefined
worldRendererConfig: WorldRendererConfig
playerStateReactive: PlayerStateReactive
playerStateUtils: PlayerStateUtils
@ -490,7 +488,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.allChunksFinished = true
this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
}
this.updateChunksStats()
}
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
@ -510,10 +507,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
timeUpdated? (newTime: number): void
biomeUpdated? (biome: any): void
biomeReset? (): void
updateViewerPosition (pos: Vec3) {
this.viewerChunkPosition = pos
for (const [key, value] of Object.entries(this.loadedChunks)) {
@ -609,6 +602,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
const resources = this.resourcesManager.currentResources
if (this.workers.length === 0) throw new Error('workers not initialized yet')
// Get instanceable blocks data if available (Three.js specific)
const instancedBlocksData = this.getInstancedBlocksData?.()
for (const [i, worker] of this.workers.entries()) {
const { blockstatesModels } = resources
@ -620,6 +617,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
},
blockstatesModels,
config: this.getMesherConfig(),
instancedBlocks: instancedBlocksData?.instanceableBlocks ? [...instancedBlocksData.instanceableBlocks] : [],
instancedBlockIds: instancedBlocksData?.allBlocksStateIdToModelIdMap || {}
})
}
@ -688,6 +687,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.checkAllFinished()
}
debugRemoveCurrentChunk () {
const [x, z] = chunkPos(this.viewerChunkPosition!)
this.removeColumn(x, z)
}
removeColumn (x, z) {
delete this.loadedChunks[`${x},${z}`]
for (const worker of this.workers) {
@ -835,9 +839,12 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
})
worldEmitter.on('time', (timeOfDay) => {
if (!this.worldRendererConfig.dayCycle) return
this.timeUpdated?.(timeOfDay)
if (timeOfDay < 0 || timeOfDay > 24_000) {
throw new Error('Invalid time of day. It should be between 0 and 24000.')
}
this.timeOfTheDay = timeOfDay
// if (this.worldRendererConfig.skyLight === skyLight) return
@ -846,14 +853,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
// (this).rerenderAllChunks?.()
// }
})
worldEmitter.on('biomeUpdate', ({ biome }) => {
this.biomeUpdated?.(biome)
})
worldEmitter.on('biomeReset', () => {
this.biomeReset?.()
})
}
setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {
@ -938,7 +937,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
return Promise.all(data)
}
setSectionDirty (pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks
setSectionDirty (pos: Vec3, value = true, useChangeWorker = false, instancingMode = InstancingMode.None) { // value false is used for unloading chunks
if (!this.forceCallFromMesherReplayer && this.mesherLogReader) return
if (this.viewDistance === -1) throw new Error('viewDistance not set')
@ -952,7 +951,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
// Dispatch sections to workers based on position
// This guarantees uniformity accross workers and that a given section
// is always dispatched to the same worker
const hash = this.getWorkerNumber(pos, useChangeWorker && this.mesherLogger.active)
const hash = this.getWorkerNumber(pos, this.mesherLogger.active)
this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
if (this.forceCallFromMesherReplayer) {
this.workers[hash].postMessage({
@ -961,17 +960,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
y: pos.y,
z: pos.z,
value,
instancingMode,
config: this.getMesherConfig(),
})
} else {
this.toWorkerMessagesQueue[hash] ??= []
this.toWorkerMessagesQueue[hash].push({
// this.workers[hash].postMessage({
type: 'dirty',
x: pos.x,
y: pos.y,
z: pos.z,
value,
instancingMode,
config: this.getMesherConfig(),
})
this.dispatchMessages()

View file

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

View file

@ -0,0 +1,743 @@
import PrismarineChatLoader from 'prismarine-chat'
import * as THREE from 'three'
import * as nbt from 'prismarine-nbt'
import { Vec3 } from 'vec3'
import { MesherGeometryOutput } from '../lib/mesher/shared'
import { chunkPos } from '../lib/simpleUtils'
import { renderSign } from '../sign-renderer'
import { getMesh } from './entity/EntityMesh'
import type { WorldRendererThree } from './worldrendererThree'
import { armorModel } from './entity/armorModels'
import { disposeObject } from './threeJsUtils'
export interface ChunkMeshPool {
mesh: THREE.Mesh
inUse: boolean
lastUsedTime: number
sectionKey?: string
}
export interface SectionObject extends THREE.Group {
mesh?: THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
tilesCount?: number
blocksCount?: number
signsContainer?: THREE.Group
headsContainer?: THREE.Group
boxHelper?: THREE.BoxHelper
fountain?: boolean
}
export class ChunkMeshManager {
private readonly meshPool: ChunkMeshPool[] = []
private readonly activeSections = new Map<string, ChunkMeshPool>()
readonly sectionObjects: Record<string, SectionObject> = {}
private poolSize: number
private maxPoolSize: number
private minPoolSize: number
private readonly signHeadsRenderer: SignHeadsRenderer
// Performance tracking
private hits = 0
private misses = 0
// Debug flag to bypass pooling
public bypassPooling = false
// Performance monitoring
private readonly renderTimes: number[] = []
private readonly maxRenderTimeSamples = 30
private _performanceOverrideDistance?: number
private lastPerformanceCheck = 0
private readonly performanceCheckInterval = 2000 // Check every 2 seconds
get performanceOverrideDistance () {
return this._performanceOverrideDistance ?? 0
}
set performanceOverrideDistance (value: number | undefined) {
this._performanceOverrideDistance = value
this.updateSectionsVisibility()
}
constructor (
public worldRenderer: WorldRendererThree,
public scene: THREE.Group,
public material: THREE.Material,
public worldHeight: number,
viewDistance = 3,
) {
this.updateViewDistance(viewDistance)
this.signHeadsRenderer = new SignHeadsRenderer(worldRenderer)
this.initializePool()
}
private initializePool () {
// Create initial pool
for (let i = 0; i < this.poolSize; i++) {
const geometry = new THREE.BufferGeometry()
const mesh = new THREE.Mesh(geometry, this.material)
mesh.visible = false
mesh.matrixAutoUpdate = false
mesh.name = 'pooled-section-mesh'
const poolEntry: ChunkMeshPool = {
mesh,
inUse: false,
lastUsedTime: 0
}
this.meshPool.push(poolEntry)
// Don't add to scene here - meshes will be added to containers
}
}
/**
* Update or create a section with new geometry data
*/
updateSection (sectionKey: string, geometryData: MesherGeometryOutput): SectionObject | null {
// Remove existing section object from scene if it exists
let sectionObject = this.sectionObjects[sectionKey]
if (sectionObject) {
this.cleanupSection(sectionKey)
}
// Get or create mesh from pool
let poolEntry = this.activeSections.get(sectionKey)
if (!poolEntry) {
poolEntry = this.acquireMesh()
if (!poolEntry) {
console.warn(`ChunkMeshManager: No available mesh in pool for section ${sectionKey}`)
return null
}
this.activeSections.set(sectionKey, poolEntry)
poolEntry.sectionKey = sectionKey
}
const { mesh } = poolEntry
// Update geometry attributes efficiently
this.updateGeometryAttribute(mesh.geometry, 'position', geometryData.positions, 3)
this.updateGeometryAttribute(mesh.geometry, 'normal', geometryData.normals, 3)
this.updateGeometryAttribute(mesh.geometry, 'color', geometryData.colors, 3)
this.updateGeometryAttribute(mesh.geometry, 'uv', geometryData.uvs, 2)
// Use direct index assignment for better performance (like before)
mesh.geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1)
// Set bounding box and sphere for the 16x16x16 section
mesh.geometry.boundingBox = new THREE.Box3(
new THREE.Vector3(-8, -8, -8),
new THREE.Vector3(8, 8, 8)
)
mesh.geometry.boundingSphere = new THREE.Sphere(
new THREE.Vector3(0, 0, 0),
Math.sqrt(3 * 8 ** 2)
)
// Position the mesh
mesh.position.set(geometryData.sx, geometryData.sy, geometryData.sz)
mesh.updateMatrix()
mesh.visible = true
mesh.name = 'mesh'
poolEntry.lastUsedTime = performance.now()
// Create or update the section object container
sectionObject = new THREE.Group() as SectionObject
sectionObject.add(mesh)
sectionObject.mesh = mesh as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
// Store metadata
sectionObject.tilesCount = geometryData.positions.length / 3 / 4
sectionObject.blocksCount = geometryData.blocksCount
try {
// Add signs container
if (Object.keys(geometryData.signs).length > 0) {
const signsContainer = new THREE.Group()
signsContainer.name = 'signs'
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(geometryData.signs)) {
const signBlockEntity = this.worldRenderer.blockEntities[posKey]
if (!signBlockEntity) continue
const [x, y, z] = posKey.split(',')
const sign = this.signHeadsRenderer.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
if (!sign) continue
signsContainer.add(sign)
}
sectionObject.add(signsContainer)
sectionObject.signsContainer = signsContainer
}
// Add heads container
if (Object.keys(geometryData.heads).length > 0) {
const headsContainer = new THREE.Group()
headsContainer.name = 'heads'
for (const [posKey, { isWall, rotation }] of Object.entries(geometryData.heads)) {
const headBlockEntity = this.worldRenderer.blockEntities[posKey]
if (!headBlockEntity) continue
const [x, y, z] = posKey.split(',')
const head = this.signHeadsRenderer.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
if (!head) continue
headsContainer.add(head)
}
sectionObject.add(headsContainer)
sectionObject.headsContainer = headsContainer
}
} catch (err) {
console.error('ChunkMeshManager: Error adding signs or heads to section', err)
}
// Store and add to scene
this.sectionObjects[sectionKey] = sectionObject
this.scene.add(sectionObject)
sectionObject.matrixAutoUpdate = false
return sectionObject
}
cleanupSection (sectionKey: string) {
// Remove section object from scene
const sectionObject = this.sectionObjects[sectionKey]
if (sectionObject) {
this.scene.remove(sectionObject)
// Dispose signs and heads containers
if (sectionObject.signsContainer) {
this.disposeContainer(sectionObject.signsContainer)
}
if (sectionObject.headsContainer) {
this.disposeContainer(sectionObject.headsContainer)
}
delete this.sectionObjects[sectionKey]
}
}
/**
* Release a section and return its mesh to the pool
*/
releaseSection (sectionKey: string): boolean {
this.cleanupSection(sectionKey)
const poolEntry = this.activeSections.get(sectionKey)
if (!poolEntry) {
return false
}
// Hide mesh and mark as available
poolEntry.mesh.visible = false
poolEntry.inUse = false
poolEntry.sectionKey = undefined
poolEntry.lastUsedTime = 0
// Clear geometry to free memory
this.clearGeometry(poolEntry.mesh.geometry)
this.activeSections.delete(sectionKey)
// Memory cleanup: if pool exceeds max size and we have free meshes, remove one
this.cleanupExcessMeshes()
return true
}
/**
* Get section object if it exists
*/
getSectionObject (sectionKey: string): SectionObject | undefined {
return this.sectionObjects[sectionKey]
}
/**
* Update box helper for a section
*/
updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material) {
const sectionObject = this.sectionObjects[sectionKey]
if (!sectionObject?.mesh) return
if (showChunkBorders) {
if (!sectionObject.boxHelper) {
// mesh with static dimensions: 16x16x16
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), chunkBoxMaterial)
staticChunkMesh.position.copy(sectionObject.mesh.position)
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
boxHelper.name = 'helper'
sectionObject.add(boxHelper)
sectionObject.name = 'chunk'
sectionObject.boxHelper = boxHelper
}
sectionObject.boxHelper.visible = true
} else if (sectionObject.boxHelper) {
sectionObject.boxHelper.visible = false
}
}
/**
* Get mesh for section if it exists
*/
getSectionMesh (sectionKey: string): THREE.Mesh | undefined {
return this.activeSections.get(sectionKey)?.mesh
}
/**
* Check if section is managed by this pool
*/
hasSection (sectionKey: string): boolean {
return this.activeSections.has(sectionKey)
}
/**
* Update pool size based on new view distance
*/
updateViewDistance (maxViewDistance: number) {
// Calculate dynamic pool size based on view distance
const chunksInView = (maxViewDistance * 2 + 1) ** 2
const maxSectionsPerChunk = this.worldHeight / 16
const avgSectionsPerChunk = 5
this.minPoolSize = Math.floor(chunksInView * avgSectionsPerChunk)
this.maxPoolSize = Math.floor(chunksInView * maxSectionsPerChunk) + 1
this.poolSize ??= this.minPoolSize
// Expand pool if needed to reach optimal size
if (this.minPoolSize > this.poolSize) {
const targetSize = Math.min(this.minPoolSize, this.maxPoolSize)
this.expandPool(targetSize)
}
console.log(`ChunkMeshManager: Updated view max distance to ${maxViewDistance}, pool: ${this.poolSize}/${this.maxPoolSize}, optimal: ${this.minPoolSize}`)
}
/**
* Get pool statistics
*/
getStats () {
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
const hitRate = this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(1) : '0'
const memoryUsage = this.getEstimatedMemoryUsage()
return {
poolSize: this.poolSize,
activeCount: this.activeSections.size,
freeCount,
hitRate: `${hitRate}%`,
hits: this.hits,
misses: this.misses,
memoryUsage
}
}
/**
* Get total tiles rendered
*/
getTotalTiles (): number {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.tilesCount || 0), 0)
}
/**
* Get total blocks rendered
*/
getTotalBlocks (): number {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.blocksCount || 0), 0)
}
/**
* Estimate memory usage in MB
*/
getEstimatedMemoryUsage (): { total: string, breakdown: any } {
let totalBytes = 0
let positionBytes = 0
let normalBytes = 0
let colorBytes = 0
let uvBytes = 0
let indexBytes = 0
for (const poolEntry of this.meshPool) {
if (poolEntry.inUse && poolEntry.mesh.geometry) {
const { geometry } = poolEntry.mesh
const position = geometry.getAttribute('position')
if (position) {
const bytes = position.array.byteLength
positionBytes += bytes
totalBytes += bytes
}
const normal = geometry.getAttribute('normal')
if (normal) {
const bytes = normal.array.byteLength
normalBytes += bytes
totalBytes += bytes
}
const color = geometry.getAttribute('color')
if (color) {
const bytes = color.array.byteLength
colorBytes += bytes
totalBytes += bytes
}
const uv = geometry.getAttribute('uv')
if (uv) {
const bytes = uv.array.byteLength
uvBytes += bytes
totalBytes += bytes
}
if (geometry.index) {
const bytes = geometry.index.array.byteLength
indexBytes += bytes
totalBytes += bytes
}
}
}
const totalMB = (totalBytes / (1024 * 1024)).toFixed(2)
return {
total: `${totalMB} MB`,
breakdown: {
position: `${(positionBytes / (1024 * 1024)).toFixed(2)} MB`,
normal: `${(normalBytes / (1024 * 1024)).toFixed(2)} MB`,
color: `${(colorBytes / (1024 * 1024)).toFixed(2)} MB`,
uv: `${(uvBytes / (1024 * 1024)).toFixed(2)} MB`,
index: `${(indexBytes / (1024 * 1024)).toFixed(2)} MB`,
}
}
}
/**
* Cleanup and dispose resources
*/
dispose () {
// Release all active sections
for (const [sectionKey] of this.activeSections) {
this.releaseSection(sectionKey)
}
// Dispose all meshes and geometries
for (const poolEntry of this.meshPool) {
// Meshes will be removed from scene when their parent containers are removed
poolEntry.mesh.geometry.dispose()
}
this.meshPool.length = 0
this.activeSections.clear()
}
// Private helper methods
private acquireMesh (): ChunkMeshPool | undefined {
if (this.bypassPooling) {
return {
mesh: new THREE.Mesh(new THREE.BufferGeometry(), this.material),
inUse: true,
lastUsedTime: performance.now()
}
}
// Find first available mesh
const availableMesh = this.meshPool.find(entry => !entry.inUse)
if (availableMesh) {
availableMesh.inUse = true
this.hits++
return availableMesh
}
// No available mesh, expand pool to accommodate new sections
let newPoolSize = Math.min(this.poolSize + 16, this.maxPoolSize)
if (newPoolSize === this.poolSize) {
newPoolSize = this.poolSize + 8
this.maxPoolSize = newPoolSize
console.warn(`ChunkMeshManager: Pool exhausted (${this.poolSize}/${this.maxPoolSize}). Emergency expansion to ${newPoolSize}`)
}
this.misses++
this.expandPool(newPoolSize)
return this.acquireMesh()
}
private expandPool (newSize: number) {
const oldSize = this.poolSize
this.poolSize = newSize
// console.log(`ChunkMeshManager: Expanding pool from ${oldSize} to ${newSize}`)
// Add new meshes to pool
for (let i = oldSize; i < newSize; i++) {
const geometry = new THREE.BufferGeometry()
const mesh = new THREE.Mesh(geometry, this.material)
mesh.visible = false
mesh.matrixAutoUpdate = false
mesh.name = 'pooled-section-mesh'
const poolEntry: ChunkMeshPool = {
mesh,
inUse: false,
lastUsedTime: 0
}
this.meshPool.push(poolEntry)
// Don't add to scene here - meshes will be added to containers
}
}
private updateGeometryAttribute (
geometry: THREE.BufferGeometry,
name: string,
array: Float32Array,
itemSize: number
) {
const attribute = geometry.getAttribute(name)
if (attribute && attribute.count === array.length / itemSize) {
// Reuse existing attribute
;(attribute.array as Float32Array).set(array)
attribute.needsUpdate = true
} else {
// Create new attribute (this will dispose the old one automatically)
geometry.setAttribute(name, new THREE.BufferAttribute(array, itemSize))
}
}
private clearGeometry (geometry: THREE.BufferGeometry) {
// Clear attributes but keep the attribute objects for reuse
const attributes = ['position', 'normal', 'color', 'uv']
for (const name of attributes) {
const attr = geometry.getAttribute(name)
if (attr) {
// Just mark as needing update but don't dispose to avoid recreation costs
attr.needsUpdate = true
}
}
if (geometry.index) {
geometry.index.needsUpdate = true
}
}
private cleanupExcessMeshes () {
// If pool size exceeds max and we have free meshes, remove some
if (this.poolSize > this.maxPoolSize) {
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
if (freeCount > 0) {
const excessCount = Math.min(this.poolSize - this.maxPoolSize, freeCount)
for (let i = 0; i < excessCount; i++) {
const freeIndex = this.meshPool.findIndex(entry => !entry.inUse)
if (freeIndex !== -1) {
const poolEntry = this.meshPool[freeIndex]
poolEntry.mesh.geometry.dispose()
this.meshPool.splice(freeIndex, 1)
this.poolSize--
}
}
// console.log(`ChunkMeshManager: Cleaned up ${excessCount} excess meshes. Pool size: ${this.poolSize}/${this.maxPoolSize}`)
}
}
}
private disposeContainer (container: THREE.Group) {
disposeObject(container, true)
}
/**
* Record render time for performance monitoring
*/
recordRenderTime (renderTime: number): void {
this.renderTimes.push(renderTime)
if (this.renderTimes.length > this.maxRenderTimeSamples) {
this.renderTimes.shift()
}
// Check performance periodically
const now = performance.now()
if (now - this.lastPerformanceCheck > this.performanceCheckInterval) {
this.checkPerformance()
this.lastPerformanceCheck = now
}
}
/**
* Get current effective render distance
*/
getEffectiveRenderDistance (): number {
return this.performanceOverrideDistance || this.worldRenderer.viewDistance
}
/**
* Force reset performance override
*/
resetPerformanceOverride (): void {
this.performanceOverrideDistance = undefined
this.renderTimes.length = 0
console.log('ChunkMeshManager: Performance override reset')
}
/**
* Get average render time
*/
getAverageRenderTime (): number {
if (this.renderTimes.length === 0) return 0
return this.renderTimes.reduce((sum, time) => sum + time, 0) / this.renderTimes.length
}
/**
* Check if performance is degraded and adjust render distance
*/
private checkPerformance (): void {
if (this.renderTimes.length < this.maxRenderTimeSamples) return
const avgRenderTime = this.getAverageRenderTime()
const targetRenderTime = 16.67 // 60 FPS target (16.67ms per frame)
const performanceThreshold = targetRenderTime * 1.5 // 25ms threshold
if (avgRenderTime > performanceThreshold) {
// Performance is bad, reduce render distance
const currentViewDistance = this.worldRenderer.viewDistance
const newDistance = Math.max(1, Math.floor(currentViewDistance * 0.8))
if (!this.performanceOverrideDistance || newDistance < this.performanceOverrideDistance) {
this.performanceOverrideDistance = newDistance
console.warn(`ChunkMeshManager: Performance degraded (${avgRenderTime.toFixed(2)}ms avg). Reducing effective render distance to ${newDistance}`)
}
} else if (this.performanceOverrideDistance && avgRenderTime < targetRenderTime * 1.1) {
// Performance is good, gradually restore render distance
const currentViewDistance = this.worldRenderer.viewDistance
const newDistance = Math.min(currentViewDistance, this.performanceOverrideDistance + 1)
if (newDistance !== this.performanceOverrideDistance) {
this.performanceOverrideDistance = newDistance >= currentViewDistance ? undefined : newDistance
console.log(`ChunkMeshManager: Performance improved. Restoring render distance to ${newDistance}`)
}
}
}
/**
* Hide sections beyond performance override distance
*/
updateSectionsVisibility (): void {
const cameraPos = this.worldRenderer.cameraSectionPos
for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
if (!this.performanceOverrideDistance) {
sectionObject.visible = true
continue
}
const [x, y, z] = sectionKey.split(',').map(Number)
const sectionPos = { x: x / 16, y: y / 16, z: z / 16 }
// Calculate distance using hypot (same as render distance calculation)
const dx = sectionPos.x - cameraPos.x
const dz = sectionPos.z - cameraPos.z
const distance = Math.floor(Math.hypot(dx, dz))
sectionObject.visible = distance <= this.performanceOverrideDistance
}
}
}
class SignHeadsRenderer {
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
constructor (public worldRendererThree: WorldRendererThree) {
}
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
const textures = blockEntity.SkullOwner?.Properties?.textures[0]
if (!textures) return
try {
const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
let skinUrl = textureData.textures?.SKIN?.url
const { skinTexturesProxy } = this.worldRendererThree.worldRendererConfig
if (skinTexturesProxy) {
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
.replace('https://textures.minecraft.net/', skinTexturesProxy)
}
const mesh = getMesh(this.worldRendererThree, skinUrl, armorModel.head)
const group = new THREE.Group()
if (isWall) {
mesh.position.set(0, 0.3125, 0.3125)
}
// move head model down as armor have a different offset than blocks
mesh.position.y -= 23 / 16
group.add(mesh)
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.scale.set(0.8, 0.8, 0.8)
return group
} catch (err) {
console.error('Error decoding player texture:', err)
}
}
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
const tex = this.getSignTexture(position, blockEntity, isHanging)
if (!tex) return
// todo implement
// const key = JSON.stringify({ position, rotation, isWall })
// if (this.signsCache.has(key)) {
// console.log('cached', key)
// } else {
// this.signsCache.set(key, tex)
// }
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
mesh.renderOrder = 999
const lineHeight = 7 / 16
const scaleFactor = isHanging ? 1.3 : 1
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
const thickness = (isHanging ? 2 : 1.5) / 16
const wallSpacing = 0.25 / 16
if (isWall && !isHanging) {
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
} else {
mesh.position.set(0, 0, thickness / 2 + 0.0001)
}
const group = new THREE.Group()
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.add(mesh)
const height = (isHanging ? 10 : 8) / 16
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
const textPosition = height / 2 + heightOffset
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
return group
}
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
const chunk = chunkPos(position)
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
if (!textures) {
textures = {}
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
}
const texturekey = `${position.x},${position.y},${position.z}`
// todo investigate bug and remove this so don't need to clean in section dirty
if (textures[texturekey]) return textures[texturekey]
const PrismarineChat = PrismarineChatLoader(this.worldRendererThree.version)
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
if (!canvas) return
const tex = new THREE.Texture(canvas)
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
tex.needsUpdate = true
textures[texturekey] = tex
return tex
}
}

View file

@ -20,9 +20,7 @@ import { ItemSpecificContextProperties } from '../lib/basePlayerState'
import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins'
import { renderComponent } from '../sign-renderer'
import { createCanvas } from '../lib/utils'
import { PlayerObjectType } from '../lib/createPlayerObject'
import { getBlockMeshFromModel } from './holdingBlock'
import { createItemMesh } from './itemMesh'
import * as Entity from './entity/EntityMesh'
import { getMesh } from './entity/EntityMesh'
import { WalkingGeneralSwing } from './entity/animations'
@ -34,6 +32,12 @@ export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
export const TWEEN_DURATION = 120
type PlayerObjectType = PlayerObject & {
animation?: PlayerAnimation
realPlayerUuid: string
realUsername: string
}
function convert2sComplementToHex (complement: number) {
if (complement < 0) {
complement = (0xFF_FF_FF_FF + complement + 1) >>> 0
@ -137,7 +141,7 @@ const addNametag = (entity, options: { fontFamily: string }, mesh, version: stri
const canvas = getUsernameTexture(entity, options, version)
const tex = new THREE.Texture(canvas)
tex.needsUpdate = true
let nameTag: THREE.Object3D
let nameTag
if (entity.nameTagFixed) {
const geometry = new THREE.PlaneGeometry()
const material = new THREE.MeshBasicMaterial({ map: tex })
@ -167,7 +171,6 @@ const addNametag = (entity, options: { fontFamily: string }, mesh, version: stri
nameTag.name = 'nametag'
mesh.add(nameTag)
return nameTag
}
}
@ -491,10 +494,6 @@ export class Entities {
// todo true/undefined doesnt reset the skin to the default one
// 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) {
const isCustomSkin = skinUrl !== stevePngUrl
if (isCustomSkin) {
this.loadedSkinEntityIds.add(String(entityId))
}
if (uuidCache) {
if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache] = {}
if (typeof skinUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].skinUrl = skinUrl
@ -736,42 +735,73 @@ export class Entities {
outerGroup.add(mesh)
return {
mesh: outerGroup,
meshGeometry: mesh.children.find(child => child instanceof THREE.Mesh)?.geometry,
isBlock: true,
itemsTexture: null,
itemsTextureFlipped: null,
modelName: textureUv.modelName,
}
}
// Render proper 3D model for items
// TODO: Render proper model (especially for blocks) instead of flat texture
if (textureUv) {
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 sizeX = su ?? 1 // su is actually width
const sizeY = sv ?? 1 // sv is actually height
// Use the new unified item mesh function
const result = createItemMesh(textureThree, {
u,
v,
sizeX,
sizeY
}, {
faceCamera,
use3D: !faceCamera, // Only use 3D for non-camera-facing items
})
const size = undefined
const itemsTexture = textureThree.clone()
itemsTexture.flipY = true
const sizeY = (sv ?? size)!
const sizeX = (su ?? size)!
itemsTexture.offset.set(u, 1 - v - sizeY)
itemsTexture.repeat.set(sizeX, sizeY)
itemsTexture.needsUpdate = true
itemsTexture.magFilter = THREE.NearestFilter
itemsTexture.minFilter = THREE.NearestFilter
let mesh: THREE.Object3D
let itemsTextureFlipped: THREE.Texture | undefined
if (faceCamera) {
const spriteMat = new THREE.SpriteMaterial({
map: itemsTexture,
transparent: true,
alphaTest: 0.1,
})
mesh = new THREE.Sprite(spriteMat)
} else {
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,
})
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
if (specificProps['minecraft:display_context'] === 'ground') {
SCALE = 0.5
} else if (specificProps['minecraft:display_context'] === 'thirdperson') {
SCALE = 6
}
result.mesh.scale.set(SCALE, SCALE, SCALE)
mesh.scale.set(SCALE, SCALE, SCALE)
return {
mesh: result.mesh,
mesh,
isBlock: false,
itemsTexture,
itemsTextureFlipped,
modelName: textureUv.modelName,
cleanup: result.cleanup
}
}
}
@ -871,9 +901,8 @@ export class Entities {
group.additionalCleanup = () => {
// important: avoid texture memory leak and gpu slowdown
if (object.cleanup) {
object.cleanup()
}
object.itemsTexture?.dispose()
object.itemsTextureFlipped?.dispose()
}
}
}
@ -884,14 +913,20 @@ export class Entities {
mesh = wrapper
if (entity.username) {
const nametag = addNametag(entity, { fontFamily: 'mojangles' }, wrapper, this.worldRenderer.version)
if (nametag) {
nametag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
nametag.scale.multiplyScalar(12)
}
// todo proper colors
const nameTag = new NameTagObject(fromFormattedString(entity.username).text, {
font: `48px ${this.entitiesOptions.fontFamily}`,
})
nameTag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
nameTag.renderOrder = 1000
nameTag.name = 'nametag'
//@ts-expect-error
wrapper.add(nameTag)
}
} else {
mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, { ...overrides, customModel: entity['customModel'] })
mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, overrides)
}
if (!mesh) return
mesh.name = 'mesh'
@ -1147,7 +1182,8 @@ export class Entities {
const cameraPos = this.worldRenderer.cameraObject.position
const distance = mesh.position.distanceTo(cameraPos)
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)
}
}
@ -1262,9 +1298,8 @@ export class Entities {
const group = new THREE.Object3D()
group['additionalCleanup'] = () => {
// important: avoid texture memory leak and gpu slowdown
if (itemObject.cleanup) {
itemObject.cleanup()
}
itemObject.itemsTexture?.dispose()
itemObject.itemsTextureFlipped?.dispose()
}
const itemMesh = itemObject.mesh
group.rotation.z = -Math.PI / 16
@ -1311,12 +1346,12 @@ export class Entities {
}
}
raycastSceneDebug () {
debugRaycastScene () {
// return any object from scene. raycast from camera
const raycaster = new THREE.Raycaster()
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children)
return intersects[0]?.object
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children.filter(child => child.visible))
return intersects.find(intersect => intersect.object.visible)?.object
}
private setupPlayerObject (entity: SceneEntity['originalEntity'], wrapper: THREE.Group, overrides: { texture?: string }): PlayerObjectType {

View file

@ -0,0 +1,30 @@
import legacyJson from '../../../src/preflatMap.json'
export const getPreflatBlock = (block, reportIssue?: () => void) => {
const b = block
b._properties = {}
const namePropsStr = legacyJson.blocks[b.type + ':' + b.metadata] || findClosestLegacyBlockFallback(b.type, b.metadata, reportIssue)
if (namePropsStr) {
b.name = namePropsStr.split('[')[0]
const propsStr = namePropsStr.split('[')?.[1]?.split(']')
if (propsStr) {
const newProperties = Object.fromEntries(propsStr.join('').split(',').map(x => {
let [key, val] = x.split('=')
if (!isNaN(val)) val = parseInt(val, 10)
return [key, val]
}))
b._properties = newProperties
}
}
return b
}
const findClosestLegacyBlockFallback = (id, metadata, reportIssue) => {
reportIssue?.()
for (const [key, value] of Object.entries(legacyJson.blocks)) {
const [idKey, meta] = key.split(':')
if (idKey === id) return value
}
return null
}

View file

@ -44,12 +44,6 @@ const getBackendMethods = (worldRenderer: WorldRendererThree) => {
shakeFromDamage: worldRenderer.cameraShake.shakeFromDamage.bind(worldRenderer.cameraShake),
onPageInteraction: worldRenderer.media.onPageInteraction.bind(worldRenderer.media),
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

@ -189,7 +189,7 @@ export default class HoldingBlock {
this.swingAnimator?.stopSwing()
}
render (originalCamera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer, ambientLight: THREE.AmbientLight, directionalLight: THREE.DirectionalLight) {
render (renderer: THREE.WebGLRenderer) {
if (!this.lastHeldItem) return
const now = performance.now()
if (this.lastUpdate && now - this.lastUpdate > 50) { // one tick
@ -205,15 +205,13 @@ export default class HoldingBlock {
this.blockSwapAnimation?.switcher.update()
const scene = new THREE.Scene()
const scene = this.worldRenderer.templateScene
scene.add(this.cameraGroup)
// if (this.camera.aspect !== originalCamera.aspect) {
// this.camera.aspect = originalCamera.aspect
// this.camera.updateProjectionMatrix()
// }
this.updateCameraGroup()
scene.add(ambientLight.clone())
scene.add(directionalLight.clone())
const viewerSize = renderer.getSize(new THREE.Vector2())
const minSize = Math.min(viewerSize.width, viewerSize.height)
@ -241,6 +239,8 @@ export default class HoldingBlock {
if (offHandDisplay) {
this.cameraGroup.scale.x = 1
}
scene.remove(this.cameraGroup)
}
// worldTest () {

View file

@ -0,0 +1,860 @@
import * as THREE from 'three'
import { Vec3 } from 'vec3'
import { versionToNumber } from 'flying-squid/dist/utils'
import PrismarineBlock from 'prismarine-block'
import { IndexedBlock } from 'minecraft-data'
import moreBlockData from '../lib/moreBlockDataGenerated.json'
import { InstancingMode, MesherGeometryOutput } from '../lib/mesher/shared'
import { getPreflatBlock } from './getPreflatBlock'
import { WorldRendererThree } from './worldrendererThree'
// Helper function to parse RGB color strings from moreBlockDataGenerated.json
function parseRgbColor (rgbString: string): number {
const match = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(rgbString)
if (!match) return 0x99_99_99 // Default gray
const r = parseInt(match[1], 10)
const g = parseInt(match[2], 10)
const b = parseInt(match[3], 10)
return (r << 16) | (g << 8) | b
}
export interface InstancedBlockData {
stateId: number
positions: Vec3[]
blockName: string
}
export interface InstancedSectionData {
sectionKey: string
instancedBlocks: Map<number, InstancedBlockData>
shouldUseInstancedOnly: boolean
}
export interface InstancedBlockModelData {
stateId: number
// textures: number[]
rotation: number[]
transparent?: boolean
emitLight?: number
filterLight?: number
textureInfos?: Array<{ u: number, v: number, su: number, sv: number }>
}
export interface InstancedBlocksConfig {
instanceableBlocks: Set<number>
blocksDataModel: Record<number, InstancedBlockModelData>
blockNameToStateIdMap: Record<string, number>
interestedTextureTiles: Set<string>
}
export class InstancedRenderer {
isPreflat: boolean
USE_APP_GEOMETRY = true
private readonly instancedMeshes = new Map<number, THREE.InstancedMesh>()
private readonly sceneUsedMeshes = new Map<string, THREE.InstancedMesh>()
private readonly blockCounts = new Map<number, number>()
private readonly sectionInstances = new Map<string, Map<number, number[]>>()
private readonly cubeGeometry: THREE.BoxGeometry
private readonly tempMatrix = new THREE.Matrix4()
private readonly stateIdToName = new Map<number, string>()
// Cache for single color materials
private readonly colorMaterials = new Map<number, THREE.MeshBasicMaterial>()
// Dynamic instance management
private readonly initialInstancesPerBlock = 2000
private readonly maxInstancesPerBlock = 100_000
private readonly maxTotalInstances = 10_000_000
private currentTotalInstances = 0
private readonly growthFactor = 1.5 // How much to grow when needed
// Visibility control
private _instancedMeshesVisible = true
// Memory tracking
private totalAllocatedInstances = 0
private instancedBlocksConfig: InstancedBlocksConfig | null = null
private sharedSolidMaterial: THREE.MeshLambertMaterial | null = null
constructor (private readonly worldRenderer: WorldRendererThree) {
this.cubeGeometry = this.createCubeGeometry()
this.isPreflat = versionToNumber(this.worldRenderer.version) < versionToNumber('1.13')
// Create shared solid material with no transparency
this.sharedSolidMaterial = new THREE.MeshLambertMaterial({
transparent: false,
alphaTest: 0.1
})
}
private getStateId (blockName: string): number {
if (!this.instancedBlocksConfig) {
throw new Error('Instanced blocks config not prepared')
}
const stateId = this.instancedBlocksConfig.blockNameToStateIdMap[blockName]
if (stateId === undefined) {
throw new Error(`Block ${blockName} not found in blockNameToStateIdMap`)
}
return stateId
}
// Add getter/setter for visibility
get instancedMeshesVisible (): boolean {
return this._instancedMeshesVisible
}
set instancedMeshesVisible (visible: boolean) {
this._instancedMeshesVisible = visible
// Update all instanced meshes visibility
for (const mesh of this.instancedMeshes.values()) {
mesh.visible = visible
}
}
private getInitialInstanceCount (blockName: string): number {
// Start with small allocation, can grow later if needed
return Math.min(this.initialInstancesPerBlock, this.maxInstancesPerBlock)
}
debugResizeMesh () {
// Debug helper to test resize operation
const blockName = 'grass_block'
const stateId = this.getStateId(blockName)
const mesh = this.instancedMeshes.get(stateId)
this.resizeInstancedMesh(stateId, mesh!.instanceMatrix.count * this.growthFactor)
}
private resizeInstancedMesh (stateId: number, newSize: number): boolean {
const mesh = this.instancedMeshes.get(stateId)
if (!mesh) return false
const blockName = this.stateIdToName.get(stateId) || 'unknown'
const oldSize = mesh.instanceMatrix.count
const actualInstanceCount = this.blockCounts.get(stateId) || 0
// console.log(`Growing instances for ${blockName}: ${oldSize} -> ${newSize} (${((newSize / oldSize - 1) * 100).toFixed(1)}% increase)`)
const { geometry } = mesh
const { material } = mesh
// Create new mesh with increased capacity
const newMesh = new THREE.InstancedMesh(
geometry,
material,
newSize
)
newMesh.name = mesh.name
newMesh.frustumCulled = false
newMesh.visible = this._instancedMeshesVisible
// Copy ALL existing instances using our tracked count
for (let i = 0; i < actualInstanceCount; i++) {
this.tempMatrix.identity()
mesh.getMatrixAt(i, this.tempMatrix)
newMesh.setMatrixAt(i, this.tempMatrix)
}
newMesh.count = actualInstanceCount
newMesh.instanceMatrix.needsUpdate = true
this.totalAllocatedInstances += (newSize - oldSize)
this.worldRenderer.scene.add(newMesh)
this.instancedMeshes.set(stateId, newMesh)
this.worldRenderer.scene.remove(mesh)
// Clean up old mesh
mesh.geometry.dispose()
if (Array.isArray(mesh.material)) {
for (const m of mesh.material) m.dispose()
} else {
mesh.material.dispose()
}
// Verify instance count matches
// console.log(`Finished growing ${blockName}. Actual instances: ${actualInstanceCount}, New capacity: ${newSize}, Mesh count: ${newMesh.count}`)
return true
}
private canAddMoreInstances (stateId: number, count: number): boolean {
const currentForBlock = this.blockCounts.get(stateId) || 0
const mesh = this.instancedMeshes.get(stateId)
if (!mesh) return false
const blockName = this.stateIdToName.get(stateId) || 'unknown'
// If we would exceed current capacity, try to grow
if (currentForBlock + count > mesh.instanceMatrix.count) {
const currentCapacity = mesh.instanceMatrix.count
const neededCapacity = currentForBlock + count
const newSize = Math.min(
this.maxInstancesPerBlock,
Math.ceil(Math.max(
neededCapacity,
currentCapacity * this.growthFactor
))
)
// console.log(`Need to grow ${blockName}: current ${currentForBlock}/${currentCapacity}, need ${neededCapacity}, growing to ${newSize}`)
// Check if growth would exceed total budget
const growthAmount = newSize - currentCapacity
if (this.totalAllocatedInstances + growthAmount > this.maxTotalInstances) {
console.warn(`Cannot grow instances for ${blockName}: would exceed total budget`)
return false
}
// Try to grow
if (!this.resizeInstancedMesh(stateId, newSize)) {
console.warn(`Failed to grow instances for ${blockName}`)
return false
}
}
// Check total instance budget
if (this.currentTotalInstances + count > this.maxTotalInstances) {
console.warn(`Total instance limit reached (${this.currentTotalInstances}/${this.maxTotalInstances})`)
return false
}
return true
}
prepareInstancedBlock (stateId: number, name: string, props: Record<string, any>, mcBlockData?: IndexedBlock, defaultState = false) {
const config = this.instancedBlocksConfig!
const possibleIssues = [] as string[]
const { currentResources } = this.worldRenderer.resourcesManager
if (!currentResources?.worldBlockProvider) return
const models = currentResources.worldBlockProvider.getAllResolvedModels0_1({
name,
properties: props
}, this.isPreflat, possibleIssues, [], [], true)
// skipping composite blocks
if (models.length !== 1 || !models[0]![0].elements) {
return
}
const elements = models[0]![0]?.elements
if (!elements || (elements.length !== 1 && name !== 'grass_block')) {
return
}
const elem = elements[0]
if (elem.from[0] !== 0 || elem.from[1] !== 0 || elem.from[2] !== 0 || elem.to[0] !== 16 || elem.to[1] !== 16 || elem.to[2] !== 16) {
// not full block
return
}
const facesMapping = [
['front', 'south'],
['bottom', 'down'],
['top', 'up'],
['right', 'east'],
['left', 'west'],
['back', 'north'],
]
const blockData: InstancedBlockModelData = {
stateId,
rotation: [0, 0, 0, 0, 0, 0],
textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({ u: 0, v: 0, su: 0, sv: 0 }))
}
for (const [face, { texture, cullface, rotation = 0 }] of Object.entries(elem.faces)) {
const faceIndex = facesMapping.findIndex(x => x.includes(face))
if (faceIndex === -1) {
throw new Error(`Unknown face ${face}`)
}
blockData.rotation[faceIndex] = rotation / 90
if (Math.floor(blockData.rotation[faceIndex]) !== blockData.rotation[faceIndex]) {
throw new Error(`Invalid rotation ${rotation} ${name}`)
}
config.interestedTextureTiles.add(texture.debugName)
// Store texture info for this face
blockData.textureInfos![faceIndex] = {
u: texture.u,
v: texture.v,
su: texture.su,
sv: texture.sv
}
}
config.blocksDataModel[stateId] = blockData
config.instanceableBlocks.add(stateId)
config.blockNameToStateIdMap[name] = stateId
if (mcBlockData) {
blockData.transparent = mcBlockData.transparent
blockData.emitLight = mcBlockData.emitLight
blockData.filterLight = mcBlockData.filterLight
}
}
prepareInstancedBlocksData () {
if (this.sharedSolidMaterial) {
this.sharedSolidMaterial.dispose()
this.sharedSolidMaterial = null
}
this.sharedSolidMaterial = new THREE.MeshLambertMaterial({
transparent: true,
// depthWrite: true,
alphaTest: 0.1
})
this.sharedSolidMaterial.map = this.worldRenderer.material.map
// this.sharedTransparentMaterial = new THREE.MeshLambertMaterial({
// transparent: true,
// // depthWrite: false,
// alphaTest: 0.1
// })
// this.sharedTransparentMaterial.map = this.worldRenderer.material.map
const { forceInstancedOnly } = this.worldRenderer.worldRendererConfig
const debugBlocksMap = forceInstancedOnly ? {
'double_stone_slab': 'stone',
'stone_slab': 'stone',
'oak_stairs': 'planks',
'stone_stairs': 'stone',
'glass_pane': 'stained_glass',
'brick_stairs': 'brick_block',
'stone_brick_stairs': 'stonebrick',
'nether_brick_stairs': 'nether_brick',
'double_wooden_slab': 'planks',
'wooden_slab': 'planks',
'sandstone_stairs': 'sandstone',
'cobblestone_wall': 'cobblestone',
'quartz_stairs': 'quartz_block',
'stained_glass_pane': 'stained_glass',
'red_sandstone_stairs': 'red_sandstone',
'stone_slab2': 'stone_slab',
'purpur_stairs': 'purpur_block',
'purpur_slab': 'purpur_block',
} : {}
const PBlockOriginal = PrismarineBlock(this.worldRenderer.version)
this.instancedBlocksConfig = {
instanceableBlocks: new Set(),
blocksDataModel: {},
blockNameToStateIdMap: {},
interestedTextureTiles: new Set(),
} satisfies InstancedBlocksConfig
// Add unknown block model
this.prepareInstancedBlock(-1, 'unknown', {})
// Handle texture overrides for special blocks
const textureOverrideFullBlocks = {
water: 'water_still',
lava: 'lava_still',
}
// Process all blocks to find instanceable ones
for (const b of loadedData.blocksArray) {
for (let stateId = b.minStateId; stateId <= b.maxStateId; stateId++) {
const config = this.instancedBlocksConfig
const mapping = debugBlocksMap[b.name]
const block = PBlockOriginal.fromStateId(mapping && loadedData.blocksByName[mapping] ? loadedData.blocksByName[mapping].defaultState : stateId, 0)
if (this.isPreflat) {
getPreflatBlock(block)
}
const textureOverride = textureOverrideFullBlocks[block.name] as string | undefined
if (textureOverride) {
const { currentResources } = this.worldRenderer.resourcesManager
if (!currentResources?.worldBlockProvider) continue
const texture = currentResources.worldBlockProvider.getTextureInfo(textureOverride)
if (!texture) {
console.warn('Missing texture override for', block.name)
continue
}
const texIndex = texture.tileIndex
config.blocksDataModel[stateId] = {
stateId,
rotation: [0, 0, 0, 0, 0, 0],
filterLight: b.filterLight,
textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({
u: texture.u,
v: texture.v,
su: texture.su,
sv: texture.sv
}))
}
config.instanceableBlocks.add(block.stateId)
config.interestedTextureTiles.add(textureOverride)
config.blockNameToStateIdMap[block.name] = stateId
continue
}
// Check if block is a full cube
if (block.shapes.length === 0 || !block.shapes.every(shape => {
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
})) {
continue
}
this.prepareInstancedBlock(stateId, block.name, block.getProperties(), b, stateId === b.defaultState)
}
}
}
private getOrCreateColorMaterial (blockName: string): THREE.Material {
const color = this.getBlockColor(blockName)
const materialKey = color
let material = this.colorMaterials.get(materialKey)
if (!material) {
material = new THREE.MeshBasicMaterial({
color,
transparent: false
})
material.name = `instanced_color_${blockName}`
this.colorMaterials.set(materialKey, material)
}
return material
}
private createBlockMaterial (blockName: string, instancingMode: InstancingMode): THREE.Material {
if (instancingMode === InstancingMode.ColorOnly) {
return this.getOrCreateColorMaterial(blockName)
} else {
return this.sharedSolidMaterial!
}
}
// Update initializeInstancedMeshes to respect visibility setting
initializeInstancedMeshes () {
if (!this.instancedBlocksConfig) {
console.warn('Instanced blocks config not prepared')
return
}
// Create InstancedMesh for each instanceable block type
for (const stateId of this.instancedBlocksConfig.instanceableBlocks) {
const blockName = this.stateIdToName.get(stateId)
if (blockName) {
this.initializeInstancedMesh(stateId, blockName, InstancingMode.ColorOnly)
}
}
}
initializeInstancedMesh (stateId: number, blockName: string, instancingMode: InstancingMode) {
if (this.instancedMeshes.has(stateId)) return // Skip if already exists
if (!this.instancedBlocksConfig!.blocksDataModel) {
this.prepareInstancedBlock(stateId, blockName, {})
}
const blockModelData = this.instancedBlocksConfig!.blocksDataModel[stateId]
const isTransparent = blockModelData?.transparent ?? false
const initialCount = this.getInitialInstanceCount(blockName)
const geometry = blockModelData ? this.createCustomGeometry(stateId, blockModelData) : this.cubeGeometry
const material = this.createBlockMaterial(blockName, instancingMode)
const mesh = new THREE.InstancedMesh(
geometry,
material,
initialCount
)
mesh.name = `instanced_${blockName}`
mesh.frustumCulled = false
mesh.count = 0
mesh.visible = this._instancedMeshesVisible // Set initial visibility
// mesh.renderOrder = isTransparent ? 1 : 0
this.instancedMeshes.set(stateId, mesh)
// Don't add to scene until actually used
this.totalAllocatedInstances += initialCount
if (!blockModelData) {
console.warn(`No block model data found for block ${blockName}`)
}
}
private debugRaycast () {
// get instanced block name
const raycaster = new THREE.Raycaster()
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children.filter(child => child.visible))
for (const intersect of intersects) {
const mesh = intersect.object as THREE.Mesh
if (mesh.name.startsWith('instanced_')) {
console.log(`Instanced block name: ${mesh.name}`)
}
}
}
private createCubeGeometry (): THREE.BoxGeometry {
// Create a basic cube geometry
// For proper texturing, we would need to modify UV coordinates per block type
// For now, use default BoxGeometry which works with the texture atlas
const geometry = new THREE.BoxGeometry(1, 1, 1)
return geometry
}
private createCustomGeometry (stateId: number, blockModelData: InstancedBlockModelData): THREE.BufferGeometry {
if (this.USE_APP_GEOMETRY) {
const itemMesh = this.worldRenderer.entities.getItemMesh(stateId === -1 ? {
name: 'unknown'
} : {
blockState: stateId
}, {})
return itemMesh?.meshGeometry
}
// Create custom geometry with specific UV coordinates per face
const geometry = new THREE.BoxGeometry(1, 1, 1)
// Get UV attribute
const uvAttribute = geometry.getAttribute('uv') as THREE.BufferAttribute
const uvs = uvAttribute.array as Float32Array
if (!blockModelData.textureInfos) {
console.warn('No texture infos available for block model')
return geometry
}
// BoxGeometry has 6 faces, each with 4 vertices (8 UV values)
// Three.js BoxGeometry face order: +X, -X, +Y, -Y, +Z, -Z
// Our face mapping: [front, bottom, top, right, left, back] = [south, down, up, east, west, north]
// Map to Three.js indices: [+Z, -Y, +Y, +X, -X, -Z] = [4, 3, 2, 0, 1, 5]
interface UVVertex {
u: number
v: number
}
for (let faceIndex = 0; faceIndex < 6; faceIndex++) {
// Map Three.js face index to our face index
let ourFaceIndex: number
switch (faceIndex) {
case 0: ourFaceIndex = 3; break // +X -> right (east)
case 1: ourFaceIndex = 4; break // -X -> left (west)
case 2: ourFaceIndex = 2; break // +Y -> top (up)
case 3: ourFaceIndex = 1; break // -Y -> bottom (down)
case 4: ourFaceIndex = 0; break // +Z -> front (south)
case 5: ourFaceIndex = 5; break // -Z -> back (north)
default: continue
}
const textureInfo = blockModelData.textureInfos[ourFaceIndex]
const rotation = blockModelData.rotation[ourFaceIndex]
if (!textureInfo) {
console.warn(`No texture info found for face ${ourFaceIndex}`)
continue
}
const { u, v, su, sv } = textureInfo
const faceUvStart = faceIndex * 8
// Get original UVs for this face
const faceUVs = uvs.slice(faceUvStart, faceUvStart + 8)
// Apply rotation if needed (0=0°, 1=90°, 2=180°, 3=270°)
// Add base 180° rotation (2) to all faces
const totalRotation = (rotation + 2) % 4
if (totalRotation > 0) {
// Each vertex has 2 UV coordinates (u,v)
// We need to rotate the 4 vertices as a group
const vertices: UVVertex[] = []
for (let i = 0; i < 8; i += 2) {
vertices.push({
u: faceUVs[i],
v: faceUVs[i + 1]
})
}
// Rotate vertices
const rotatedVertices: UVVertex[] = []
for (let i = 0; i < 4; i++) {
const srcIndex = (i + totalRotation) % 4
rotatedVertices.push(vertices[srcIndex])
}
// Write back rotated coordinates
for (let i = 0; i < 4; i++) {
faceUVs[i * 2] = rotatedVertices[i].u
faceUVs[i * 2 + 1] = rotatedVertices[i].v
}
}
// Apply texture atlas coordinates to the potentially rotated UVs
for (let i = 0; i < 8; i += 2) {
uvs[faceUvStart + i] = u + faceUVs[i] * su
uvs[faceUvStart + i + 1] = v + faceUVs[i + 1] * sv
}
}
uvAttribute.needsUpdate = true
return geometry
}
private getBlockColor (blockName: string): number {
// Get color from moreBlockDataGenerated.json
const rgbString = moreBlockData.colors[blockName]
if (rgbString) {
return parseRgbColor(rgbString)
}
// Debug: Log when color is not found
console.warn(`No color found for block: ${blockName}, using default gray`)
// Fallback to default gray if color not found
return 0x99_99_99
}
handleInstancedBlocksFromWorker (instancedBlocks: MesherGeometryOutput['instancedBlocks'], sectionKey: string, instancingMode: InstancingMode) {
// Initialize section tracking if not exists
if (!this.sectionInstances.has(sectionKey)) {
this.sectionInstances.set(sectionKey, new Map())
}
const sectionMap = this.sectionInstances.get(sectionKey)!
// Remove old instances for blocks that are being updated
const previousStateIds = [...sectionMap.keys()]
for (const stateId of previousStateIds) {
const instanceIndices = sectionMap.get(stateId)
if (instanceIndices) {
this.removeInstancesFromBlock(stateId, instanceIndices)
sectionMap.delete(stateId)
}
}
// Keep track of blocks that were updated this frame
for (const [blockName, blockData] of Object.entries(instancedBlocks)) {
const { stateId, positions, matrices } = blockData
this.stateIdToName.set(stateId, blockName)
if (this.USE_APP_GEOMETRY) {
this.initializeInstancedMesh(stateId, blockName, instancingMode)
}
const instanceIndices: number[] = []
const currentCount = this.blockCounts.get(stateId) || 0
// Check if we can add all positions at once
const neededInstances = positions.length
if (!this.canAddMoreInstances(stateId, neededInstances)) {
console.warn(`Cannot add ${neededInstances} instances for block ${blockName} (current: ${currentCount}, max: ${this.maxInstancesPerBlock})`)
continue
}
const mesh = this.instancedMeshes.get(stateId)!
// Add new instances for this section using pre-calculated matrices from worker
for (let i = 0; i < positions.length; i++) {
const instanceIndex = currentCount + instanceIndices.length
mesh.setMatrixAt(instanceIndex, new THREE.Matrix4().fromArray(matrices[i]))
instanceIndices.push(instanceIndex)
}
// Update tracking
if (instanceIndices.length > 0) {
sectionMap.set(stateId, instanceIndices)
const newCount = currentCount + instanceIndices.length
this.blockCounts.set(stateId, newCount)
this.currentTotalInstances += instanceIndices.length
mesh.count = newCount
mesh.instanceMatrix.needsUpdate = true
// Only add mesh to scene when it's first used
if (newCount === instanceIndices.length) {
this.worldRenderer.scene.add(mesh)
}
this.sceneUsedMeshes.set(blockName, mesh)
}
}
}
removeSectionInstances (sectionKey: string) {
const sectionMap = this.sectionInstances.get(sectionKey)
if (!sectionMap) return // Section not tracked
// Remove instances for each block type in this section
for (const [stateId, instanceIndices] of sectionMap) {
this.removeInstancesFromBlock(stateId, instanceIndices)
// Remove from sceneUsedMeshes if no instances left
const blockName = this.stateIdToName.get(stateId)
if (blockName && (this.blockCounts.get(stateId) || 0) === 0) {
this.sceneUsedMeshes.delete(blockName)
}
}
// Remove section from tracking
this.sectionInstances.delete(sectionKey)
}
private removeInstancesFromBlock (stateId: number, indicesToRemove: number[]) {
const mesh = this.instancedMeshes.get(stateId)
if (!mesh || indicesToRemove.length === 0) return
const currentCount = this.blockCounts.get(stateId) || 0
const removeSet = new Set(indicesToRemove)
// Update total instance count
this.currentTotalInstances -= indicesToRemove.length
// Create mapping from old indices to new indices
const indexMapping = new Map<number, number>()
let writeIndex = 0
const tempMatrix = new THREE.Matrix4()
// Compact the instance matrix by removing gaps
for (let readIndex = 0; readIndex < currentCount; readIndex++) {
if (!removeSet.has(readIndex)) {
indexMapping.set(readIndex, writeIndex)
if (writeIndex !== readIndex) {
mesh.getMatrixAt(readIndex, tempMatrix)
mesh.setMatrixAt(writeIndex, tempMatrix)
}
writeIndex++
}
}
// Update count
const newCount = writeIndex
this.blockCounts.set(stateId, newCount)
mesh.count = newCount
mesh.instanceMatrix.needsUpdate = true
// Update all section tracking to reflect new indices
for (const [sectionKey, sectionMap] of this.sectionInstances) {
const sectionIndices = sectionMap.get(stateId)
if (sectionIndices) {
const updatedIndices = sectionIndices
.map(index => indexMapping.get(index))
.filter(index => index !== undefined)
if (updatedIndices.length > 0) {
sectionMap.set(stateId, updatedIndices)
} else {
sectionMap.delete(stateId)
}
}
}
// Update sceneUsedMeshes if no instances left
if (newCount === 0) {
const blockName = this.stateIdToName.get(stateId)
if (blockName) {
this.sceneUsedMeshes.delete(blockName)
}
}
}
disposeOldMeshes () {
// Reset total instance count since we're clearing everything
this.currentTotalInstances = 0
for (const [stateId, mesh] of this.instancedMeshes) {
if (mesh.material instanceof THREE.Material && mesh.material.name.startsWith('instanced_color_')) {
mesh.material.dispose()
}
mesh.geometry.dispose()
this.instancedMeshes.delete(stateId)
this.worldRenderer.scene.remove(mesh)
}
// Clear counts
this.blockCounts.clear()
}
destroy () {
// Clean up resources
for (const [stateId, mesh] of this.instancedMeshes) {
this.worldRenderer.scene.remove(mesh)
mesh.geometry.dispose()
if (mesh.material instanceof THREE.Material) {
mesh.material.dispose()
}
}
// Clean up materials
if (this.sharedSolidMaterial) {
this.sharedSolidMaterial.dispose()
this.sharedSolidMaterial = null
}
for (const material of this.colorMaterials.values()) {
material.dispose()
}
this.colorMaterials.clear()
this.instancedMeshes.clear()
this.blockCounts.clear()
this.sectionInstances.clear()
this.stateIdToName.clear()
this.sceneUsedMeshes.clear()
this.cubeGeometry.dispose()
}
// Add visibility info to stats
getStats () {
let totalInstances = 0
let activeBlockTypes = 0
let totalWastedMemory = 0
for (const [stateId, mesh] of this.instancedMeshes) {
const allocated = mesh.instanceMatrix.count
const used = mesh.count
totalWastedMemory += (allocated - used) * 64 // 64 bytes per instance (approximate)
if (used > 0) {
totalInstances += used
activeBlockTypes++
}
}
const maxPerBlock = this.maxInstancesPerBlock
const renderDistance = this.worldRenderer.viewDistance
return {
totalInstances,
activeBlockTypes,
drawCalls: this._instancedMeshesVisible ? activeBlockTypes : 0,
memoryStats: {
totalAllocatedInstances: this.totalAllocatedInstances,
usedInstances: totalInstances,
wastedInstances: this.totalAllocatedInstances - totalInstances,
estimatedMemoryUsage: this.totalAllocatedInstances * 64,
estimatedWastedMemory: totalWastedMemory,
utilizationPercent: ((totalInstances / this.totalAllocatedInstances) * 100).toFixed(1) + '%'
},
maxInstancesPerBlock: maxPerBlock,
totalInstanceBudget: this.maxTotalInstances,
renderDistance,
instanceUtilization: totalInstances / this.maxTotalInstances,
instancedMeshesVisible: this._instancedMeshesVisible
}
}
// New method to prepare and initialize everything
prepareAndInitialize () {
console.log('Preparing instanced blocks data...')
this.prepareInstancedBlocksData()
const config = this.instancedBlocksConfig!
console.log(`Found ${config.instanceableBlocks.size} instanceable blocks`)
this.disposeOldMeshes()
this.initializeInstancedMeshes()
}
// Method to get the current configuration
getInstancedBlocksConfig (): InstancedBlocksConfig | null {
return this.instancedBlocksConfig
}
}

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

@ -204,7 +204,7 @@ export class PanoramaRenderer {
}
)
if (this.worldRenderer instanceof WorldRendererThree) {
this.scene = this.worldRenderer.scene
this.scene = this.worldRenderer.realScene
}
void worldView.init(initPos)

View file

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

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

@ -565,7 +565,7 @@ export class ThreeJsMedia {
raycaster.setFromCamera(mouse, camera)
// Check intersection with all objects in scene
const intersects = raycaster.intersectObjects(scene.children, true)
const intersects = raycaster.intersectObjects(scene.children.filter(child => child.visible), true)
if (intersects.length > 0) {
const intersection = intersects[0]
const intersectedObject = intersection.object

View file

@ -2,7 +2,7 @@ import * as THREE from 'three'
import { WorldRendererThree } from './worldrendererThree'
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
}
@ -10,17 +10,7 @@ export class ThreeJsSound implements SoundSystem {
audioListener: THREE.AudioListener | undefined
private readonly activeSounds = new Set<THREE.PositionalAudio>()
private readonly audioContext: AudioContext | undefined
private readonly soundVolumes = new Map<THREE.PositionalAudio, number>()
baseVolume = 1
constructor (public worldRenderer: WorldRendererThree) {
worldRenderer.onWorldSwitched.push(() => {
this.stopAll()
})
worldRenderer.onReactiveConfigUpdated('volume', (volume) => {
this.changeVolume(volume)
})
}
initAudioListener () {
@ -29,24 +19,20 @@ export class ThreeJsSound implements SoundSystem {
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()
const sound = new THREE.PositionalAudio(this.audioListener!)
this.activeSounds.add(sound)
this.soundVolumes.set(sound, volume)
const audioLoader = new THREE.AudioLoader()
const start = Date.now()
void audioLoader.loadAsync(path).then((buffer) => {
if (Date.now() - start > timeout) {
console.warn('Ignored playing sound', path, 'due to timeout:', timeout, 'ms <', Date.now() - start, 'ms')
return
}
if (Date.now() - start > 500) return
// play
sound.setBuffer(buffer)
sound.setRefDistance(20)
sound.setVolume(volume * this.baseVolume)
sound.setVolume(volume)
sound.setPlaybackRate(pitch) // set the pitch
this.worldRenderer.scene.add(sound)
// set sound position
@ -57,35 +43,21 @@ export class ThreeJsSound implements SoundSystem {
sound.disconnect()
}
this.activeSounds.delete(sound)
this.soundVolumes.delete(sound)
audioLoader.manager.itemEnd(path)
}
sound.play()
})
}
stopAll () {
destroy () {
// Stop and clean up all active sounds
for (const sound of this.activeSounds) {
if (!sound) continue
sound.stop()
if (sound.source) {
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
if (this.audioListener) {
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
interactionLines: null | { blockPos: Vec3, mesh: THREE.Group, shapePositions: BlocksShapes | undefined } = null
interactionLines: null | { blockPos: Vec3, mesh: THREE.Group } = null
prevColor: string | undefined
blockBreakMesh: THREE.Mesh
breakTextures: THREE.Texture[] = []
@ -62,13 +62,6 @@ export class CursorBlock {
this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => {
this.updateLineMaterial()
})
// todo figure out why otherwise fog from skybox breaks it
setTimeout(() => {
this.updateLineMaterial()
if (this.interactionLines) {
this.setHighlightCursorBlock(this.interactionLines.blockPos, this.interactionLines.shapePositions, true)
}
})
}
// Update functions
@ -76,9 +69,6 @@ export class CursorBlock {
const inCreative = this.worldRenderer.playerStateReactive.gameMode === 'creative'
const pixelRatio = this.worldRenderer.renderer.getPixelRatio()
if (this.cursorLineMaterial) {
this.cursorLineMaterial.dispose()
}
this.cursorLineMaterial = new LineMaterial({
color: (() => {
switch (this.worldRenderer.worldRendererConfig.highlightBlockColor) {
@ -125,8 +115,8 @@ export class CursorBlock {
}
}
setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes, force = false): void {
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos) && !force) {
setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes): void {
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
return
}
if (this.interactionLines !== null) {
@ -150,7 +140,7 @@ export class CursorBlock {
}
this.worldRenderer.scene.add(group)
group.visible = !this.cursorLinesHidden
this.interactionLines = { blockPos, mesh: group, shapePositions }
this.interactionLines = { blockPos, mesh: group }
}
render () {

View file

@ -1,22 +1,17 @@
import * as THREE from 'three'
import { Vec3 } from 'vec3'
import nbt from 'prismarine-nbt'
import PrismarineChatLoader from 'prismarine-chat'
import * as tweenJs from '@tweenjs/tween.js'
import { Biome } from 'minecraft-data'
import { renderSign } from '../sign-renderer'
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
import { chunkPos, sectionPos } from '../lib/simpleUtils'
import { sectionPos } from '../lib/simpleUtils'
import { WorldRendererCommon } from '../lib/worldrendererCommon'
import { WorldDataEmitterWorker } from '../lib/worldDataEmitter'
import { addNewStat } from '../lib/ui/newStats'
import { MesherGeometryOutput } from '../lib/mesher/shared'
import { MesherGeometryOutput, InstancingMode } from '../lib/mesher/shared'
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
import { getMyHand } from './hand'
import HoldingBlock from './holdingBlock'
import { getMesh } from './entity/EntityMesh'
import { armorModel } from './entity/armorModels'
import { disposeObject, loadThreeJsTextureFromBitmap } from './threeJsUtils'
import { loadThreeJsTextureFromBitmap } from './threeJsUtils'
import { CursorBlock } from './world/cursorBlock'
import { getItemUv } from './appShared'
import { Entities } from './entities'
@ -24,21 +19,23 @@ import { ThreeJsSound } from './threeJsSound'
import { CameraShake } from './cameraShake'
import { ThreeJsMedia } from './threeJsMedia'
import { Fountain } from './threeJsParticles'
import { WaypointsRenderer } from './waypoints'
import { DEFAULT_TEMPERATURE, SkyboxRenderer } from './skyboxRenderer'
import { InstancedRenderer } from './instancedRenderer'
import { ChunkMeshManager } from './chunkMeshManager'
type SectionKey = string
export class WorldRendererThree extends WorldRendererCommon {
outputFormat = 'threeJs' as const
sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean }> = {}
sectionInstancingMode: Record<string, InstancingMode> = {}
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
signsCache = new Map<string, any>()
starField: StarField
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
holdingBlock: HoldingBlock
holdingBlockLeft: HoldingBlock
scene = new THREE.Scene()
holdingBlock: HoldingBlock | undefined
holdingBlockLeft: HoldingBlock | undefined
realScene = new THREE.Scene()
scene = new THREE.Group()
templateScene = new THREE.Scene()
ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
entities = new Entities(this)
@ -50,10 +47,12 @@ export class WorldRendererThree extends WorldRendererCommon {
cameraShake: CameraShake
cameraContainer: THREE.Object3D
media: ThreeJsMedia
instancedRenderer: InstancedRenderer | undefined
chunkMeshManager: ChunkMeshManager
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
waypoints: WaypointsRenderer
camera: THREE.PerspectiveCamera
renderTimeAvg = 0
chunkBoxMaterial = new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })
sectionsOffsetsAnimations = {} as {
[chunkKey: string]: {
time: number,
@ -73,17 +72,17 @@ export class WorldRendererThree extends WorldRendererCommon {
}
fountains: Fountain[] = []
DEBUG_RAYCAST = false
skyboxRenderer: SkyboxRenderer
private currentPosTween?: tweenJs.Tween<THREE.Vector3>
private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
private readonly worldOffset = new THREE.Vector3()
get tilesRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
return this.chunkMeshManager.getTotalTiles()
}
get blocksRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0)
return this.chunkMeshManager.getTotalBlocks()
}
constructor (public renderer: THREE.WebGLRenderer, public initOptions: GraphicsInitOptions, public displayOptions: DisplayWorldOptions) {
@ -97,18 +96,19 @@ export class WorldRendererThree extends WorldRendererCommon {
this.holdingBlock = new HoldingBlock(this)
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.resetScene()
void this.init()
this.soundSystem = new ThreeJsSound(this)
this.cameraShake = new CameraShake(this, this.onRender)
this.media = new ThreeJsMedia(this)
this.waypoints = new WaypointsRenderer(this)
this.instancedRenderer = new InstancedRenderer(this)
this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
// Enable bypass pooling for debugging if URL param is present
if (new URLSearchParams(location.search).get('bypassMeshPooling') === 'true') {
this.chunkMeshManager.bypassPooling = true
console.log('ChunkMeshManager: Bypassing pooling for debugging')
}
// this.fountain = new Fountain(this.scene, this.scene, {
// position: new THREE.Vector3(0, 10, 0),
@ -118,6 +118,14 @@ export class WorldRendererThree extends WorldRendererCommon {
this.finishChunk(chunkKey)
})
this.worldSwitchActions()
void this.init()
}
// Add this method to update world origin
private updateWorldOrigin (pos: THREE.Vector3) {
// this.worldOffset.copy(pos)
// this.scene.position.copy(this.worldOffset).multiplyScalar(-1)
}
get cameraObject () {
@ -130,8 +138,15 @@ export class WorldRendererThree extends WorldRendererCommon {
this.protocolCustomBlocks.clear()
// Reset section animations
this.sectionsOffsetsAnimations = {}
// Clear waypoints
this.waypoints.clear()
})
}
override connect (worldView: WorldDataEmitterWorker) {
super.connect(worldView)
// Add additional renderDistance handling for mesh pool updates
worldView.on('renderDistance', (viewDistance) => {
this.chunkMeshManager.updateViewDistance(viewDistance)
})
}
@ -156,36 +171,44 @@ export class WorldRendererThree extends WorldRendererCommon {
this.entities.handlePlayerEntity(e)
}
resetTemplateScene () {
this.templateScene = new THREE.Scene()
this.templateScene.add(this.ambientLight.clone())
this.templateScene.add(this.directionalLight.clone())
}
resetScene () {
this.scene.matrixAutoUpdate = false // for perf
this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
this.scene.add(this.ambientLight)
this.realScene.background = new THREE.Color(this.initOptions.config.sceneBackground)
this.realScene.add(this.ambientLight)
this.directionalLight.position.set(1, 1, 0.5).normalize()
this.directionalLight.castShadow = true
this.scene.add(this.directionalLight)
this.realScene.add(this.directionalLight)
const size = this.renderer.getSize(new THREE.Vector2())
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
this.cameraContainer = new THREE.Object3D()
this.cameraContainer.add(this.camera)
this.scene.add(this.cameraContainer)
this.realScene.add(this.cameraContainer)
this.realScene.add(this.scene)
this.resetTemplateScene()
}
override watchReactivePlayerState () {
super.watchReactivePlayerState()
this.onReactivePlayerStateUpdated('inWater', (value) => {
this.skyboxRenderer.updateWaterState(value, this.playerStateReactive.waterBreathing)
})
this.onReactivePlayerStateUpdated('waterBreathing', (value) => {
this.skyboxRenderer.updateWaterState(this.playerStateReactive.inWater, value)
this.realScene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null
})
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
if (!value) return
this.ambientLight.intensity = value
this.resetTemplateScene()
})
this.onReactivePlayerStateUpdated('directionalLight', (value) => {
if (!value) return
this.directionalLight.intensity = value
this.resetTemplateScene()
})
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
@ -201,22 +224,34 @@ export class WorldRendererThree extends WorldRendererCommon {
})
}
getInstancedBlocksData () {
const config = this.instancedRenderer?.getInstancedBlocksConfig()
if (!config) return undefined
return {
instanceableBlocks: config.instanceableBlocks,
}
}
override watchReactiveConfig () {
super.watchReactiveConfig()
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
this.updateShowChunksBorder(value)
this.updateShowChunksBorder()
})
this.onReactiveConfigUpdated('defaultSkybox', (value) => {
this.skyboxRenderer.updateDefaultSkybox(value)
this.onReactiveConfigUpdated('enableDebugOverlay', (value) => {
if (!value) {
// restore visibility
this.chunkMeshManager.updateSectionsVisibility()
}
})
}
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
if (isAnimationPlaying) {
holdingBlock.startSwing()
holdingBlock?.startSwing()
} else {
holdingBlock.stopSwing()
holdingBlock?.stopSwing()
}
}
@ -243,8 +278,12 @@ export class WorldRendererThree extends WorldRendererCommon {
oldItemsTexture.dispose()
}
// Prepare and initialize instanced renderer with dynamic block detection
this.instancedRenderer?.prepareAndInitialize()
await super.updateAssetsData()
this.onAllTexturesLoaded()
if (Object.keys(this.loadedChunks).length > 0) {
console.log('rerendering chunks because of texture update')
this.rerenderAllChunks()
@ -252,14 +291,18 @@ export class WorldRendererThree extends WorldRendererCommon {
}
onAllTexturesLoaded () {
this.holdingBlock.ready = true
this.holdingBlock.updateItem()
this.holdingBlockLeft.ready = true
this.holdingBlockLeft.updateItem()
if (this.holdingBlock) {
this.holdingBlock.ready = true
this.holdingBlock.updateItem()
}
if (this.holdingBlockLeft) {
this.holdingBlockLeft.ready = true
this.holdingBlockLeft.updateItem()
}
}
changeBackgroundColor (color: [number, number, number]): void {
this.scene.background = new THREE.Color(color[0], color[1], color[2])
this.realScene.background = new THREE.Color(color[0], color[1], color[2])
}
timeUpdated (newTime: number): void {
@ -271,19 +314,6 @@ export class WorldRendererThree extends WorldRendererCommon {
} else {
this.starField.remove()
}
this.skyboxRenderer.updateTime(newTime)
}
biomeUpdated (biome: Biome): void {
if (biome?.temperature !== undefined) {
this.skyboxRenderer.updateTemperature(biome.temperature)
}
}
biomeReset (): void {
// Reset to default temperature when biome is unknown
this.skyboxRenderer.updateTemperature(DEFAULT_TEMPERATURE)
}
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
@ -326,12 +356,20 @@ export class WorldRendererThree extends WorldRendererCommon {
const formatBigNumber = (num: number) => {
return new Intl.NumberFormat('en-US', {}).format(num)
}
const instancedStats = this.instancedRenderer?.getStats()
let text = ''
text += `C: ${formatBigNumber(this.renderer.info.render.calls)} `
text += `TR: ${formatBigNumber(this.renderer.info.render.triangles)} `
text += `TE: ${formatBigNumber(this.renderer.info.memory.textures)} `
text += `F: ${formatBigNumber(this.tilesRendered)} `
text += `B: ${formatBigNumber(this.blocksRendered)}`
text += `B: ${formatBigNumber(this.blocksRendered)} `
if (instancedStats) {
text += `I: ${formatBigNumber(instancedStats.totalInstances)}/${instancedStats.activeBlockTypes}t `
text += `DC: ${formatBigNumber(instancedStats.drawCalls)} `
}
const poolStats = this.chunkMeshManager.getStats()
const poolMode = this.chunkMeshManager.bypassPooling ? 'BYPASS' : poolStats.hitRate
text += `MP: ${poolStats.activeCount}/${poolStats.poolSize} ${poolMode}`
pane.updateText(text)
this.backendInfoReport = text
}
@ -345,8 +383,8 @@ export class WorldRendererThree extends WorldRendererCommon {
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
// sum of distances: x + y + z
const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')!
section.renderOrder = 500 - chunkDistance
const sectionObject = this.chunkMeshManager.getSectionObject(key)!
sectionObject.mesh!.renderOrder = 500 - chunkDistance
}
override updateViewerPosition (pos: Vec3): void {
@ -355,10 +393,22 @@ export class WorldRendererThree extends WorldRendererCommon {
cameraSectionPositionUpdate () {
// eslint-disable-next-line guard-for-in
for (const key in this.sectionObjects) {
const value = this.sectionObjects[key]
if (!value) continue
this.updatePosDataChunk(key)
for (const key in this.sectionInstancingMode) {
const sectionObject = this.chunkMeshManager.getSectionObject(key)!
if (sectionObject) {
this.updatePosDataChunk(key)
}
if (this.worldRendererConfig.dynamicInstancing) {
const [x, y, z] = key.split(',').map(x => +x)
const pos = new Vec3(x, y, z)
const instancingMode = this.getInstancingMode(pos)
if (instancingMode !== this.sectionInstancingMode[key]) {
// console.log('update section', key, this.sectionInstancingMode[key], '->', instancingMode)
// update section
this.setSectionDirty(pos)
}
}
}
}
@ -369,120 +419,65 @@ export class WorldRendererThree extends WorldRendererCommon {
finishChunk (chunkKey: string) {
for (const sectionKey of this.waitingChunksToDisplay[chunkKey] ?? []) {
this.sectionObjects[sectionKey].visible = true
const sectionObject = this.chunkMeshManager.getSectionObject(sectionKey)
if (sectionObject) {
sectionObject.visible = true
}
}
delete this.waitingChunksToDisplay[chunkKey]
}
// debugRecomputedDeletedObjects = 0
handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void {
if (data.type !== 'geometry') return
let object: THREE.Object3D = this.sectionObjects[data.key]
if (object) {
this.scene.remove(object)
disposeObject(object)
delete this.sectionObjects[data.key]
}
const chunkCoords = data.key.split(',')
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
const chunkKey = chunkCoords[0] + ',' + chunkCoords[2]
// if (object) {
// this.debugRecomputedDeletedObjects++
// }
const hasInstancedBlocks = data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3))
geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3))
geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3))
geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2))
geometry.index = new THREE.BufferAttribute(data.geometry.indices as Uint32Array | Uint16Array, 1)
this.instancedRenderer?.removeSectionInstances(data.key)
const mesh = new THREE.Mesh(geometry, this.material)
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
mesh.name = 'mesh'
object = new THREE.Group()
object.add(mesh)
// mesh with static dimensions: 16x16x16
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 }))
staticChunkMesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
boxHelper.name = 'helper'
object.add(boxHelper)
object.name = 'chunk';
(object as any).tilesCount = data.geometry.positions.length / 3 / 4;
(object as any).blocksCount = data.geometry.blocksCount
if (!this.displayOptions.inWorldRenderingConfig.showChunkBorders) {
boxHelper.visible = false
// Handle instanced blocks data from worker
if (hasInstancedBlocks) {
this.instancedRenderer?.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key, this.getInstancingMode(new Vec3(chunkCoords[0], chunkCoords[1], chunkCoords[2])))
}
// should not compute it once
if (Object.keys(data.geometry.signs).length) {
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) {
const signBlockEntity = this.blockEntities[posKey]
if (!signBlockEntity) continue
const [x, y, z] = posKey.split(',')
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
if (!sign) continue
object.add(sign)
}
// Check if chunk should be loaded and has geometry
if (!this.loadedChunks[chunkKey] || !data.geometry.positions.length || !this.active) {
// Release any existing section from the pool
this.chunkMeshManager.releaseSection(data.key)
return
}
if (Object.keys(data.geometry.heads).length) {
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.heads)) {
const headBlockEntity = this.blockEntities[posKey]
if (!headBlockEntity) continue
const [x, y, z] = posKey.split(',')
const head = this.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
if (!head) continue
object.add(head)
}
// Use ChunkMeshManager for optimized mesh handling
const sectionObject = this.chunkMeshManager.updateSection(data.key, data.geometry)
if (!sectionObject) {
return
}
this.sectionObjects[data.key] = object
this.updateBoxHelper(data.key)
// Handle chunk-based rendering
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
object.visible = false
sectionObject.visible = false
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
this.waitingChunksToDisplay[chunkKey] ??= []
this.waitingChunksToDisplay[chunkKey].push(data.key)
if (this.finishedChunks[chunkKey]) {
// todo it might happen even when it was not an update
this.finishChunk(chunkKey)
}
}
this.updatePosDataChunk(data.key)
object.matrixAutoUpdate = false
mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => {
// mesh.matrixAutoUpdate = false
}
this.scene.add(object)
}
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
const chunk = chunkPos(position)
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
if (!textures) {
textures = {}
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
}
const texturekey = `${position.x},${position.y},${position.z}`
// todo investigate bug and remove this so don't need to clean in section dirty
if (textures[texturekey]) return textures[texturekey]
const PrismarineChat = PrismarineChatLoader(this.version)
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
if (!canvas) return
const tex = new THREE.Texture(canvas)
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
tex.needsUpdate = true
textures[texturekey] = tex
return tex
}
getCameraPosition () {
const worldPos = new THREE.Vector3()
this.camera.getWorldPosition(worldPos)
return worldPos
// Add world offset to get true world position
return worldPos.add(this.worldOffset)
}
getSectionCameraPosition () {
@ -505,6 +500,17 @@ export class WorldRendererThree extends WorldRendererCommon {
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
const yOffset = this.playerStateReactive.eyeHeight
if (pos) {
// Convert Vec3 to THREE.Vector3
const worldPos = new THREE.Vector3(pos.x, pos.y + yOffset, pos.z)
// Update world origin before updating camera
this.updateWorldOrigin(worldPos)
// Keep camera at origin and move world instead
// this.cameraObject.position.set(pos.x, pos.y + yOffset, pos.z)
}
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
this.media.tryIntersectMedia()
this.updateCameraSectionPos()
@ -549,7 +555,7 @@ export class WorldRendererThree extends WorldRendererCommon {
raycaster.far = distance // Limit raycast distance
// Filter to only nearby chunks for performance
const nearbyChunks = Object.values(this.sectionObjects)
const nearbyChunks = Object.values(this.chunkMeshManager.sectionObjects)
.filter(obj => obj.name === 'chunk' && obj.visible)
.filter(obj => {
// Get the mesh child which has the actual geometry
@ -707,7 +713,7 @@ export class WorldRendererThree extends WorldRendererCommon {
chunksRenderBelowOverride !== undefined ||
chunksRenderDistanceOverride !== undefined
) {
for (const [key, object] of Object.entries(this.sectionObjects)) {
for (const [key, object] of Object.entries(this.chunkMeshManager.sectionObjects)) {
const [x, y, z] = key.split(',').map(Number)
const isVisible =
// eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean
@ -719,10 +725,6 @@ export class WorldRendererThree extends WorldRendererCommon {
object.visible = isVisible
}
} else {
for (const object of Object.values(this.sectionObjects)) {
object.visible = true
}
}
}
@ -734,10 +736,6 @@ export class WorldRendererThree extends WorldRendererCommon {
this.cursorBlock.render()
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
if (sizeOrFovChanged) {
const size = this.renderer.getSize(new THREE.Vector2())
@ -752,7 +750,7 @@ export class WorldRendererThree extends WorldRendererCommon {
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
this.renderer.render(this.scene, cam)
this.renderer.render(this.realScene, cam)
if (
this.displayOptions.inWorldRenderingConfig.showHand &&
@ -761,111 +759,36 @@ export class WorldRendererThree extends WorldRendererCommon {
// !this.freeFlyMode &&
!this.renderer.xr.isPresenting
) {
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
this.holdingBlock?.render(this.renderer)
this.holdingBlockLeft?.render(this.renderer)
}
for (const fountain of this.fountains) {
if (this.sectionObjects[fountain.sectionId] && !this.sectionObjects[fountain.sectionId].foutain) {
fountain.createParticles(this.sectionObjects[fountain.sectionId])
this.sectionObjects[fountain.sectionId].foutain = true
const sectionObject = this.chunkMeshManager.getSectionObject(fountain.sectionId)
if (sectionObject && !sectionObject.fountain) {
fountain.createParticles(sectionObject)
sectionObject.fountain = true
}
fountain.render()
}
this.waypoints.render()
for (const onRender of this.onRender) {
onRender()
}
const end = performance.now()
const totalTime = end - start
if (this.worldRendererConfig.autoLowerRenderDistance) {
// Record render time for performance monitoring
this.chunkMeshManager.recordRenderTime(totalTime)
}
this.renderTimeAvgCount++
this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
this.currentRenderedFrames++
}
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
let textureData: string
if (blockEntity.SkullOwner) {
textureData = blockEntity.SkullOwner.Properties?.textures?.[0]?.Value
} else {
textureData = blockEntity.profile?.properties?.find(p => p.name === 'textures')?.value
}
if (!textureData) return
try {
const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString())
let skinUrl = decodedData.textures?.SKIN?.url
const { skinTexturesProxy } = this.worldRendererConfig
if (skinTexturesProxy) {
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
.replace('https://textures.minecraft.net/', skinTexturesProxy)
}
const mesh = getMesh(this, skinUrl, armorModel.head)
const group = new THREE.Group()
if (isWall) {
mesh.position.set(0, 0.3125, 0.3125)
}
// move head model down as armor have a different offset than blocks
mesh.position.y -= 23 / 16
group.add(mesh)
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.scale.set(0.8, 0.8, 0.8)
return group
} catch (err) {
console.error('Error decoding player texture:', err)
}
}
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
const tex = this.getSignTexture(position, blockEntity, isHanging)
if (!tex) return
// todo implement
// const key = JSON.stringify({ position, rotation, isWall })
// if (this.signsCache.has(key)) {
// console.log('cached', key)
// } else {
// this.signsCache.set(key, tex)
// }
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
mesh.renderOrder = 999
const lineHeight = 7 / 16
const scaleFactor = isHanging ? 1.3 : 1
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
const thickness = (isHanging ? 2 : 1.5) / 16
const wallSpacing = 0.25 / 16
if (isWall && !isHanging) {
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
} else {
mesh.position.set(0, 0, thickness / 2 + 0.0001)
}
const group = new THREE.Group()
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.add(mesh)
const height = (isHanging ? 10 : 8) / 16
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
const textPosition = height / 2 + heightOffset
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
return group
}
lightUpdate (chunkX: number, chunkZ: number) {
// set all sections in the chunk dirty
@ -875,26 +798,27 @@ export class WorldRendererThree extends WorldRendererCommon {
}
rerenderAllChunks () { // todo not clear what to do with loading chunks
for (const key of Object.keys(this.sectionObjects)) {
for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) {
const [x, y, z] = key.split(',').map(Number)
this.setSectionDirty(new Vec3(x, y, z))
}
}
updateShowChunksBorder (value: boolean) {
for (const object of Object.values(this.sectionObjects)) {
for (const child of object.children) {
if (child.name === 'helper') {
child.visible = value
}
}
updateShowChunksBorder () {
for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) {
this.updateBoxHelper(key)
}
}
updateBoxHelper (key: string) {
const { showChunkBorders } = this.worldRendererConfig
this.chunkMeshManager.updateBoxHelper(key, showChunkBorders, this.chunkBoxMaterial)
}
resetWorld () {
super.resetWorld()
for (const mesh of Object.values(this.sectionObjects)) {
for (const mesh of Object.values(this.chunkMeshManager.sectionObjects)) {
this.scene.remove(mesh)
}
@ -911,7 +835,7 @@ export class WorldRendererThree extends WorldRendererCommon {
getLoadedChunksRelative (pos: Vec3, includeY = false) {
const [currentX, currentY, currentZ] = sectionPos(pos)
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
return Object.fromEntries(Object.entries(this.chunkMeshManager.sectionObjects).map(([key, o]) => {
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
@ -928,12 +852,13 @@ export class WorldRendererThree extends WorldRendererCommon {
}
readdChunks () {
for (const key of Object.keys(this.sectionObjects)) {
this.scene.remove(this.sectionObjects[key])
const { sectionObjects } = this.chunkMeshManager
for (const key of Object.keys(sectionObjects)) {
this.scene.remove(sectionObjects[key])
}
setTimeout(() => {
for (const key of Object.keys(this.sectionObjects)) {
this.scene.add(this.sectionObjects[key])
for (const key of Object.keys(sectionObjects)) {
this.scene.add(sectionObjects[key])
}
}, 500)
}
@ -945,6 +870,11 @@ export class WorldRendererThree extends WorldRendererCommon {
}
}
removeCurrentChunk () {
const currentChunk = this.cameraSectionPos
this.removeColumn(currentChunk.x * 16, currentChunk.z * 16)
}
removeColumn (x, z) {
super.removeColumn(x, z)
@ -952,19 +882,47 @@ export class WorldRendererThree extends WorldRendererCommon {
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
this.setSectionDirty(new Vec3(x, y, z), false)
const key = `${x},${y},${z}`
const mesh = this.sectionObjects[key]
if (mesh) {
this.scene.remove(mesh)
disposeObject(mesh)
}
delete this.sectionObjects[key]
// Remove instanced blocks for this section
this.instancedRenderer?.removeSectionInstances(key)
// Release section from mesh pool (this will also remove from scene)
this.chunkMeshManager.releaseSection(key)
}
}
setSectionDirty (...args: Parameters<WorldRendererCommon['setSectionDirty']>) {
const [pos] = args
getInstancingMode (pos: Vec3) {
const { useInstancedRendering, enableSingleColorMode, forceInstancedOnly, dynamicInstancing, dynamicInstancingModeDistance, dynamicColorModeDistance } = this.worldRendererConfig
let instancingMode = InstancingMode.None
if (useInstancedRendering || enableSingleColorMode) {
instancingMode = enableSingleColorMode
? InstancingMode.ColorOnly
: forceInstancedOnly
? InstancingMode.BlockInstancingOnly
: InstancingMode.BlockInstancing
} else if (dynamicInstancing) {
const dx = pos.x / 16 - this.cameraSectionPos.x
const dz = pos.z / 16 - this.cameraSectionPos.z
const distance = Math.floor(Math.hypot(dx, dz))
// console.log('distance', distance, `${pos.x},${pos.y},${pos.z}`)
if (distance > dynamicColorModeDistance) {
instancingMode = InstancingMode.ColorOnly
} else if (distance > dynamicInstancingModeDistance) {
instancingMode = InstancingMode.BlockInstancingOnly
}
}
return instancingMode
}
setSectionDirty (pos: Vec3, value = true) {
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
super.setSectionDirty(...args)
const instancingMode = this.getInstancingMode(pos)
super.setSectionDirty(pos, value, undefined, instancingMode)
if (value) {
this.sectionInstancingMode[`${pos.x},${pos.y},${pos.z}`] = instancingMode
}
}
static getRendererInfo (renderer: THREE.WebGLRenderer) {
@ -981,8 +939,9 @@ export class WorldRendererThree extends WorldRendererCommon {
}
destroy (): void {
this.instancedRenderer?.destroy()
this.chunkMeshManager.dispose()
super.destroy()
this.skyboxRenderer.dispose()
}
shouldObjectVisible (object: THREE.Object3D) {
@ -994,7 +953,7 @@ export class WorldRendererThree extends WorldRendererCommon {
const chunkKey = `${chunkX},${chunkZ}`
const sectionKey = `${chunkX},${sectionY},${chunkZ}`
return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey]
return !!this.finishedChunks[chunkKey] || !!this.chunkMeshManager.sectionObjects[sectionKey]
}
updateSectionOffsets () {
@ -1032,7 +991,7 @@ export class WorldRendererThree extends WorldRendererCommon {
}
// Apply the offset to the section object
const section = this.sectionObjects[key]
const section = this.chunkMeshManager.sectionObjects[key]
if (section) {
section.position.set(
anim.currentOffsetX,
@ -1066,13 +1025,6 @@ class StarField {
constructor (
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 () {
@ -1083,6 +1035,7 @@ class StarField {
const count = 7000
const factor = 7
const saturation = 10
const speed = 0.2
const geometry = new THREE.BufferGeometry()
@ -1115,6 +1068,11 @@ class StarField {
this.points = new THREE.Points(geometry, material)
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
}

View file

@ -240,10 +240,6 @@ const appConfig = defineConfig({
prep()
})
build.onAfterBuild(async () => {
if (fs.readdirSync('./assets/customTextures').length > 0) {
childProcess.execSync('tsx ./scripts/patchAssets.ts', { stdio: 'inherit' })
}
if (SINGLE_FILE_BUILD) {
// check that only index.html is in the dist/single folder
const singleBuildFiles = fs.readdirSync('./dist/single')

View file

@ -90,19 +90,16 @@ const dataTypeBundling = {
},
blocks: {
arrKey: 'name',
processData(current, prev, _, version) {
processData(current, prev) {
for (const block of current) {
const prevBlock = prev?.find(x => x.name === block.name)
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 prevBlock = prev?.find(x => x.name === block.name)
if (forceOpaque || (prevBlock && !prevBlock.transparent)) {
block.transparent = false
}
}
if (block.hardness === 0 && prevBlock && prevBlock.hardness > 0) {
block.hardness = prevBlock.hardness
}
}
}
// ignoreRemoved: true,
@ -371,7 +368,6 @@ console.log('size', fs.lstatSync(filePath).size / 1000 / 1000, gzipSizeFromFileS
const { defaultVersion } = MCProtocol
const data = MinecraftData(defaultVersion)
console.log('defaultVersion', defaultVersion, !!data)
const initialMcData = {
[defaultVersion]: {
version: data.version,

View file

@ -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
peerJsServer?: string
peerJsServerFallback?: string
promoteServers?: Array<{ ip, description, name?, version?, }>
promoteServers?: Array<{ ip, description, version? }>
mapsProvider?: string
appParams?: Record<string, any> // query string params

View file

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

View file

@ -17,8 +17,6 @@ import { options } from './optionsStorage'
import { ResourcesManager, ResourcesManagerTransferred } from './resourcesManager'
import { watchOptionsAfterWorldViewInit } from './watchOptions'
import { loadMinecraftData } from './connect'
import { reloadChunks } from './utils'
import { displayClientChat } from './botUtils'
export interface RendererReactiveState {
world: {
@ -199,13 +197,6 @@ export class AppViewer {
this.currentDisplay = 'world'
const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0)
this.worldView = new WorldDataEmitter(world, renderDistance, startPosition)
this.worldView.panicChunksReload = () => {
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
watchOptionsAfterWorldViewInit(this.worldView)
this.appConfigUdpate()

View file

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

View file

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

View file

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

View file

@ -504,7 +504,9 @@ const alwaysPressedHandledCommand = (command: Command) => {
lockUrl()
}
if (command === 'communication.toggleMicrophone') {
toggleMicrophoneMuted?.()
if (typeof toggleMicrophoneMuted === 'function') {
toggleMicrophoneMuted()
}
}
}

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,41 @@ import PItem from 'prismarine-item'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
import { options } from './optionsStorage'
import { jeiCustomCategories } from './inventoryWindows'
import { registerIdeChannels } from './core/ideChannels'
export default () => {
customEvents.on('mineflayerBotCreated', async () => {
if (!options.customChannels) return
bot.once('login', () => {
registerBlockModelsChannel()
registerMediaChannels()
registerSectionAnimationChannels()
registeredJeiChannel()
registerBlockInteractionsCustomizationChannel()
registerWaypointChannels()
registerIdeChannels()
await new Promise(resolve => {
bot.once('login', () => {
resolve(true)
})
})
registerBlockModelsChannel()
registerMediaChannels()
registerSectionAnimationChannels()
registeredJeiChannel()
sendTokenChannel()
})
}
const sendTokenChannel = () => {
const CHANNEL_NAME = 'minecraft-web-client:token'
bot._client.registerChannel(CHANNEL_NAME, [
'container',
[
{
name: 'token',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'userId',
type: ['pstring', { countType: 'i16' }]
},
]
], true)
bot._client.writeChannel(CHANNEL_NAME, {
token: new URLSearchParams(window.location.search).get('token') ?? '',
userId: new URLSearchParams(window.location.search).get('userId') ?? ''
})
}
@ -33,95 +54,6 @@ const registerChannel = (channelName: string, packetStructure: any[], handler: (
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 CHANNEL_NAME = 'minecraft-web-client:blockmodels'

View file

@ -1,7 +1,6 @@
//@ts-check
import * as nbt from 'prismarine-nbt'
import { options } from './optionsStorage'
//@ts-check
const { EventEmitter } = require('events')
const debug = require('debug')('minecraft-protocol')
const states = require('minecraft-protocol/src/states')
@ -52,20 +51,8 @@ class CustomChannelClient extends EventEmitter {
this.emit('state', newProperty, oldProperty)
}
end(endReason, fullReason) {
// eslint-disable-next-line unicorn/no-this-assignment
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
end(reason) {
this._endReason = reason
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.dayCycleAndLighting ? bot.time.timeOfDay : 0
// todo check actual colors
const dayColorRainy = { r: 111 / 255, g: 156 / 255, b: 236 / 255 }
// todo yes, we should make animations (and rain)
// eslint-disable-next-line unicorn/numeric-separators-style
const dayColor = bot.isRaining ? dayColorRainy : { r: 0.6784313725490196, g: 0.8470588235294118, b: 0.9019607843137255 } // lightblue
// let newColor = dayColor
let int = 1
if (timeProgress < evening) {
// stay dayily
} else if (timeProgress < night) {
const progressNorm = timeProgress - evening
const progressMax = night - evening
int = 1 - progressNorm / progressMax
} else if (timeProgress < morningStart) {
int = 0
} else if (timeProgress < morningEnd) {
const progressNorm = timeProgress - morningStart
const progressMax = night - morningEnd
int = progressNorm / progressMax
}
// todo need to think wisely how to set these values & also move directional light around!
const colorInt = Math.max(int, 0.1)
updateBackground({ r: dayColor.r * colorInt, g: dayColor.g * colorInt, b: dayColor.b * colorInt })
if (!options.newVersionsLighting && bot.supportFeature('blockStateId')) {
appViewer.playerState.reactive.ambientLight = Math.max(int, 0.25)
appViewer.playerState.reactive.directionalLight = Math.min(int, 0.5)
}
}
bot.on('time', timeUpdated)
timeUpdated()
}

View file

@ -16,8 +16,7 @@ export const defaultOptions = {
chatOpacityOpened: 100,
messagesLimit: 200,
volume: 50,
enableMusic: true,
musicVolume: 50,
enableMusic: false,
// fov: 70,
fov: 75,
defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front',
@ -42,9 +41,14 @@ export const defaultOptions = {
renderEars: true,
lowMemoryMode: false,
starfieldRendering: true,
defaultSkybox: true,
enabledResourcepack: null as string | null,
useVersionsTextures: 'latest',
// Instanced rendering options
useInstancedRendering: false,
autoLowerRenderDistance: false,
forceInstancedOnly: false,
instancedOnlyDistance: 6,
enableSingleColorMode: false,
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
showHand: true,
viewBobbing: true,
@ -79,17 +83,13 @@ export const defaultOptions = {
frameLimit: false as number | false,
alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null,
alwaysShowMobileControls: false,
excludeCommunicationDebugEvents: [] as string[],
excludeCommunicationDebugEvents: [],
preventDevReloadWhilePlaying: false,
numWorkers: 4,
localServerOptions: {
gameMode: 1
} as any,
saveLoginPassword: 'prompt' as 'prompt' | 'never' | 'always',
preferLoadReadonly: false,
experimentalClientSelfReload: false,
remoteSoundsSupport: false,
remoteSoundsLoadTimeout: 500,
disableLoadPrompts: false,
guestUsername: 'guest',
askGuestName: true,

View file

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

View file

@ -3,7 +3,6 @@ import fs from 'fs'
import * as nbt from 'prismarine-nbt'
import RegionFile from 'prismarine-provider-anvil/src/region'
import { versions } from 'minecraft-data'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
import { openWorldDirectory, openWorldZip } from './browserfs'
import { isGameActive } from './globalState'
import { showNotification } from './react/NotificationProvider'
@ -13,9 +12,6 @@ const parseNbt = promisify(nbt.parse)
const simplifyNbt = nbt.simplify
window.nbt = nbt
// Supported image types for skybox
const VALID_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp']
// todo display drop zone
for (const event of ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop']) {
window.addEventListener(event, (e: any) => {
@ -49,34 +45,6 @@ window.addEventListener('drop', async e => {
})
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')) {
void openWorldZip(file)
return

View file

@ -126,28 +126,6 @@ customEvents.on('gameLoaded', () => {
if (entityStatus === EntityStatus.HURT) {
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) => {
@ -246,29 +224,22 @@ customEvents.on('gameLoaded', () => {
}
}
// even if not found, still record to cache
void getThreeJsRendererMethods()!.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
void getThreeJsRendererMethods()?.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
} catch (err) {
reportError(new Error('Error applying skin texture:', { cause: err }))
console.error('Error decoding player texture:', err)
}
}
bot.on('playerJoined', 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)) {
if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) {
bot.emit('entityUpdate', entity)
}
}
}
bot.on('teamUpdated', teamUpdated)
for (const team of Object.values(bot.teams)) {
teamUpdated(team)
}
})
const updateEntityNameTags = (team: Team) => {
for (const entity of Object.values(bot.entities)) {
@ -317,7 +288,7 @@ customEvents.on('gameLoaded', () => {
})
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
// Player's team was removed, need to update all entities that are in a 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
}
}

View file

@ -56,12 +56,13 @@ import { isCypress } from './standaloneUtils'
import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
import defaultServerOptions from './defaultLocalServerOptions'
import dayCycle from './dayCycle'
import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack'
import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer'
import CustomChannelClient from './customClient'
import { registerServiceWorker } from './serviceWorker'
import { appStatusState, lastConnectOptions, quickDevReconnect } from './react/AppStatusProvider'
import { appStatusState, lastConnectOptions } from './react/AppStatusProvider'
import { fsState } from './loadSave'
import { watchFov } from './rendererUtils'
@ -214,13 +215,8 @@ export async function connect (connectOptions: ConnectOptions) {
const destroyAll = (wasKicked = false) => {
if (ended) return
loadingTimerState.loading = false
const { alwaysReconnect } = appQueryParams
if ((!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) || (alwaysReconnect)) {
if (alwaysReconnect === 'quick' || alwaysReconnect === 'fast') {
quickDevReconnect()
} else {
location.reload()
}
if (!wasKicked && miscUiState.appConfig?.allowAutoConnect && appQueryParams.autoConnect && hadConnected) {
location.reload()
}
errorAbortController.abort()
ended = true
@ -235,12 +231,8 @@ export async function connect (connectOptions: ConnectOptions) {
bot.emit('end', '')
bot.removeAllListeners()
bot._client.removeAllListeners()
bot._client = {
//@ts-expect-error
write (packetName) {
console.warn('Tried to write packet', packetName, 'after bot was destroyed')
}
}
//@ts-expect-error TODO?
bot._client = undefined
//@ts-expect-error
window.bot = bot = undefined
}
@ -304,7 +296,7 @@ export async function connect (connectOptions: ConnectOptions) {
if (connectOptions.server && !connectOptions.viewerWsConnect && !parsedServer.isWebSocket) {
console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`)
net['setProxy']({ hostname: proxy.host, port: proxy.port, headers: { Authorization: `Bearer ${new URLSearchParams(location.search).get('token') ?? ''}` }, 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
@ -793,6 +785,7 @@ export async function connect (connectOptions: ConnectOptions) {
}
initMotionTracking()
dayCycle()
// Bot position callback
const botPosition = () => {
@ -960,7 +953,7 @@ const maybeEnterGame = () => {
}
}
if (appQueryParams.reconnect && localStorage.lastConnectOptions && process.env.NODE_ENV === 'development') {
if (appQueryParams.reconnect && process.env.NODE_ENV === 'development') {
const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {})
return waitForConfigFsLoad(async () => {
void connect({

View file

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

View file

@ -1,46 +1,13 @@
import net from 'net'
import { Client } from 'minecraft-protocol'
import { appQueryParams } from '../appParams'
import { downloadAllMinecraftData, getVersionAutoSelect } from '../connect'
import { gameAdditionalState } from '../globalState'
import { ProgressReporter } from '../core/progressReporter'
import { parseServerAddress } from '../parseServerAddress'
import { getCurrentProxy } from '../react/ServersList'
import { pingServerVersion, validatePacket } from './minecraft-protocol-extra'
import { getWebsocketStream } from './websocket-core'
let lastPacketTime = 0
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
if (!appQueryParams.noPacketsValidation) {
(bot._client as unknown as Client).on('packet', (data, packetMeta, buffer, fullBuffer) => {
@ -68,7 +35,7 @@ setInterval(() => {
}, 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()
const isWebSocket = ip.startsWith('ws://') || ip.startsWith('wss://')
let stream
@ -76,8 +43,6 @@ export const getServerInfo = async (ip: string, port?: number, preferredVersion
progressReporter?.setMessage('Connecting to WebSocket server')
stream = (await getWebsocketStream(ip)).mineflayerStream
progressReporter?.setMessage('WebSocket connected. Ping packet sent, waiting for response')
} else if (setProxyParams) {
setProxy(setProxyParams)
}
window.setLoadingMessage = (message?: string) => {
if (message === undefined) {
@ -94,46 +59,3 @@ export const getServerInfo = async (ip: string, port?: number, preferredVersion
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 })
bot.mouse.beforeUpdateChecks = () => {
if (!document.hasFocus() || !isGameActive(true)) {
if (!document.hasFocus()) {
// deactive all buttons
bot.mouse.buttons.fill(false)
}

View file

@ -15,12 +15,9 @@ class CustomDuplex extends Duplex {
}
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 hostURL = new URL(`${baseProtocol}://${hostClean}`)
const hostParams = hostURL.searchParams
hostParams.append('client_mcraft', '')
const ws = new WebSocket(`${baseProtocol}://${hostURL.host}${hostURL.pathname}?${hostParams.toString()}`)
const ws = new WebSocket(`${baseProtocol}://${hostClean}`)
const clientDuplex = new CustomDuplex(undefined, data => {
ws.send(data)
})

View file

@ -6,7 +6,7 @@ import { versionToNumber } from 'mc-assets/dist/utils'
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage'
import Button from './react/Button'
import { OptionMeta, OptionSlider } from './react/OptionsItems'
import { OptionButton, OptionMeta, OptionSlider } from './react/OptionsItems'
import Slider from './react/Slider'
import { getScreenRefreshRate } from './utils'
import { setLoadingScreenStatus } from './appStatus'
@ -114,6 +114,52 @@ export const guiOptionsScheme: {
text: 'Performance Debug',
}
},
{
custom () {
let status = 'OFF'
const { useInstancedRendering, forceInstancedOnly, enableSingleColorMode } = useSnapshot(options)
if (useInstancedRendering) {
status = 'ON'
if (enableSingleColorMode) {
status = 'ON (single color)'
} else if (forceInstancedOnly) {
status = 'ON (force)'
}
}
return <OptionButton
item={{
type: 'toggle',
text: 'Instacing',
requiresChunksReload: true,
}}
cacheKey='instacing'
valueText={status}
onClick={() => {
// cycle
if (useInstancedRendering) {
if (enableSingleColorMode) {
options.useInstancedRendering = false
options.enableSingleColorMode = false
options.forceInstancedOnly = false
} else if (forceInstancedOnly) {
options.useInstancedRendering = true
options.enableSingleColorMode = true
options.forceInstancedOnly = false
} else {
options.useInstancedRendering = true
options.enableSingleColorMode = false
options.forceInstancedOnly = true
}
} else {
options.useInstancedRendering = true
options.enableSingleColorMode = false
options.forceInstancedOnly = false
}
}}
/>
},
},
{
custom () {
const { _renderByChunks } = useSnapshot(options).rendererSharedOptions
@ -480,24 +526,6 @@ export const guiOptionsScheme: {
],
sound: [
{ volume: {} },
{
custom () {
return <OptionSlider
valueOverride={options.enableMusic ? undefined : 0}
onChange={(value) => {
options.musicVolume = value
}}
item={{
type: 'slider',
id: 'musicVolume',
text: 'Music Volume',
min: 0,
max: 100,
unit: '%',
}}
/>
},
},
{
custom () {
return <Button label='Sound Muffler' onClick={() => showModal({ reactType: 'sound-muffler' })} inScreen />
@ -568,16 +596,6 @@ export const guiOptionsScheme: {
return <Category>Server Connection</Category>
},
},
{
saveLoginPassword: {
tooltip: 'Controls whether to save login passwords for servers in this browser memory.',
values: [
'prompt',
'always',
'never'
]
},
},
{
custom () {
const { serversAutoVersionSelect } = useSnapshot(options)

View file

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

View file

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

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 () => {
const lastState = useRef(JSON.parse(JSON.stringify(appStatusState)))
const currentState = useSnapshot(appStatusState)
@ -116,6 +105,13 @@ export default () => {
}
}, [isOpen])
const reconnect = () => {
resetAppStatusState()
window.dispatchEvent(new window.CustomEvent('connect', {
detail: lastConnectOptions.value
}))
}
useEffect(() => {
const controller = new AbortController()
window.addEventListener('keyup', (e) => {
@ -123,7 +119,7 @@ export default () => {
if (activeModalStack.at(-1)?.reactType !== 'app-status') return
// todo do only if reconnect is possible
if (e.code !== 'KeyR' || !lastConnectOptions.value) return
quickDevReconnect()
reconnect()
}, {
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'])
if (!account) return
lastConnectOptions.value!.authenticatedAccount = accounts.find(acc => acc.username === account) || true
quickDevReconnect()
reconnect()
}
const lastAutoCapturedPackets = getLastAutoCapturedPackets()
@ -188,7 +184,7 @@ export default () => {
actionsSlot={
<>
{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} />}
{wasDisconnected && lastAutoCapturedPackets && <Button label={`Inspect Last ${lastAutoCapturedPackets} Packets`} onClick={() => downloadAutoCapturedPackets()} />}
</>

View file

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

View file

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

View file

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

View file

@ -2,8 +2,6 @@ import { useEffect, useState } from 'react'
import { useUtilsEffect } from '@zardoy/react-util'
import { WorldRendererCommon } from 'renderer/viewer/lib/worldrendererCommon'
import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree'
import { Vec3 } from 'vec3'
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
import Screen from './Screen'
import ChunksDebug, { ChunkDebug } from './ChunksDebug'
import { useIsModalActive } from './utilsApp'
@ -14,10 +12,6 @@ const Inner = () => {
const [update, setUpdate] = useState(0)
useUtilsEffect(({ interval }) => {
const up = () => {
// setUpdate(u => u + 1)
}
bot.on('chunkColumnLoad', up)
interval(
500,
() => {
@ -26,48 +20,17 @@ const Inner = () => {
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 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 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 {
x,
z,
x: Number(key.split(',')[0]),
z: Number(key.split(',')[1]),
state,
lines: [line, line2],
lines: [String(chunk?.loads.length ?? 0)],
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}`,
],
}
@ -92,22 +55,14 @@ const Inner = () => {
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 = [
...chunksWaitingServer,
...chunksWaitingClient,
...clientProcessingChunks,
...chunksDone,
...chunksDoneEmpty,
...chunksWaitingOrder,
]
return <Screen title={`Chunks Debug (avg: ${worldView!.lastChunkReceiveTimeAvg.toFixed(1)}ms)`}>
return <Screen title="Chunks Debug">
<ChunksDebug
chunks={allChunks}
playerChunk={{

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

@ -115,7 +115,7 @@ const HotbarInner = () => {
container.current.appendChild(inv.canvas)
const upHotbarItems = () => {
if (!appViewer.resourcesManager?.itemsAtlasParser) return
globalThis.debugHotbarItems = upInventoryItems(true, inv)
upInventoryItems(true, inv)
}
canvasManager.canvas.onclick = (e) => {
@ -127,7 +127,6 @@ const HotbarInner = () => {
}
}
globalThis.debugUpHotbarItems = upHotbarItems
upHotbarItems()
bot.inventory.on('updateSlot', upHotbarItems)
appViewer.resourcesManager.on('assetsTexturesUpdated', upHotbarItems)

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

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

@ -16,7 +16,7 @@ const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererComm
const { reactiveDebugParams } = worldRenderer
const { chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled, chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, stopRendering, disableEntities } = useSnapshot(reactiveDebugParams)
const { rendererPerfDebugOverlay } = useSnapshot(options)
const { rendererPerfDebugOverlay, useInstancedRendering, forceInstancedOnly, instancedOnlyDistance, enableSingleColorMode } = useSnapshot(options)
// Helper to round values to nearest step
const roundToStep = (value: number, step: number) => Math.round(value / step) * step
@ -115,5 +115,36 @@ const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererComm
/>
</div> */}
</div>
<div className={styles.column}>
<h3>Instanced Rendering</h3>
<Button
label={useInstancedRendering ? 'Disable Instanced Rendering' : 'Enable Instanced Rendering'}
onClick={() => { options.useInstancedRendering = !options.useInstancedRendering }}
overlayColor={useInstancedRendering ? 'green' : undefined}
/>
<Button
label={forceInstancedOnly ? 'Disable Force Instanced Only' : 'Enable Force Instanced Only'}
onClick={() => { options.forceInstancedOnly = !options.forceInstancedOnly }}
overlayColor={forceInstancedOnly ? 'orange' : undefined}
/>
<Button
label={enableSingleColorMode ? 'Disable Single Color Mode' : 'Enable Single Color Mode'}
onClick={() => { options.enableSingleColorMode = !options.enableSingleColorMode }}
overlayColor={enableSingleColorMode ? 'yellow' : undefined}
/>
<Slider
label="Instanced Distance"
min={1}
max={16}
style={{ width: '100%', }}
value={instancedOnlyDistance}
updateValue={(value) => {
options.instancedOnlyDistance = Math.round(value)
}}
unit=""
valueDisplay={instancedOnlyDistance}
/>
</div>
</div>
}

View file

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

View file

@ -1,5 +1,5 @@
// Slider.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react'
import React, { useState, useEffect } from 'react'
import styles from './slider.module.css'
import SharedHudVars from './SharedHudVars'
@ -12,7 +12,6 @@ interface Props extends React.ComponentProps<'div'> {
min?: number;
max?: number;
disabledReason?: string;
throttle?: number | false; // milliseconds, default 100, false to disable
updateValue?: (value: number) => void;
updateOnDragEnd?: boolean;
@ -27,24 +26,15 @@ const Slider: React.FC<Props> = ({
min = 0,
max = 100,
disabledReason,
throttle = 0,
updateOnDragEnd = false,
updateValue,
...divProps
}) => {
label = translate(label)
disabledReason = translate(disabledReason)
valueDisplay = typeof valueDisplay === 'string' ? translate(valueDisplay) : valueDisplay
const [value, setValue] = useState(valueProp)
const getRatio = (v = value) => Math.max(Math.min((v - min) / (max - min), 1), 0)
const [ratio, setRatio] = useState(getRatio())
// Throttling refs
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const lastValueRef = useRef<number>(valueProp)
useEffect(() => {
setValue(valueProp)
}, [valueProp])
@ -52,52 +42,14 @@ const Slider: React.FC<Props> = ({
setRatio(getRatio())
}, [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) => {
throttledUpdateValue(v, dragEnd)
if (updateOnDragEnd !== dragEnd) return
updateValue?.(v)
}
const labelText = `${label}: ${valueDisplay ?? value} ${unit}`
return (
<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
type="range"
className={styles.slider}
@ -124,7 +76,7 @@ const Slider: React.FC<Props> = ({
<div className={styles.disabled} title={disabledReason} />
<div className={styles['slider-thumb']} style={{ left: `calc((100% * ${ratio}) - (8px * ${ratio}))` }} />
<label className={styles.label}>
{labelText}
{label}: {valueDisplay ?? value} {unit}
</label>
</div>
</SharedHudVars>

View file

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

View file

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

View file

@ -11,7 +11,6 @@ import { showNotification } from '../react/NotificationProvider'
import { pixelartIcons } from '../react/PixelartIcon'
import { createSoundMap, SoundMap } from './soundsMap'
import { musicSystem } from './musicSystem'
import './customSoundSystem'
let soundMap: SoundMap | undefined
@ -51,9 +50,8 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
appViewer.backend?.soundSystem?.playSound(
position,
soundData.url,
soundData.volume,
Math.max(Math.min(pitch ?? 1, 2), 0.5),
soundData.timeout ?? options.remoteSoundsLoadTimeout
soundData.volume * (options.volume / 100),
Math.max(Math.min(pitch ?? 1, 2), 0.5)
)
}
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 soundData = await soundMap.getSoundUrl(randomMusicKey)
if (!soundData || !soundMap) return
if (!soundData) return
await musicSystem.playMusic(soundData.url, soundData.volume)
}
@ -111,9 +109,6 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
}
bot.on('soundEffectHeard', async (soundId, position, volume, pitch) => {
if (/^https?:/.test(soundId.replace('minecraft:', ''))) {
return
}
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
async playMusic (url: string, musicVolume = 1) {
if (!options.enableMusic || this.currentMusic || options.musicVolume === 0) return
if (!options.enableMusic || this.currentMusic) return
try {
const { onEnded } = await loadOrPlaySound(url, musicVolume, 5000, undefined, true) ?? {}
const { onEnded } = await loadOrPlaySound(url, 0.5 * musicVolume, 5000) ?? {}
if (!onEnded) return

View file

@ -35,7 +35,6 @@ interface ResourcePackSoundEntry {
name: string
stream?: boolean
volume?: number
timeout?: number
}
interface ResourcePackSound {
@ -141,7 +140,7 @@ export class SoundMap {
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
if (this.activeResourcePackSoundsJson && soundKey in this.activeResourcePackSoundsJson) {
const rpSound = this.activeResourcePackSoundsJson[soundKey]
@ -152,13 +151,6 @@ export class SoundMap {
if (this.activeResourcePackBasePath) {
const tryFormat = async (format: string) => {
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 fileData = await fs.promises.readFile(resourcePackPath)
return {

View file

@ -3,7 +3,6 @@
import { subscribeKey } from 'valtio/utils'
import { isMobile } from 'renderer/viewer/lib/simpleUtils'
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
import { setSkinsConfig } from 'renderer/viewer/lib/utils/skins'
import { options, watchValue } from './optionsStorage'
import { reloadChunks } from './utils'
import { miscUiState } from './globalState'
@ -81,10 +80,6 @@ export const watchOptionsAfterViewerInit = () => {
updateFpsLimit(o)
})
watchValue(options, o => {
appViewer.inWorldRenderingConfig.volume = Math.max(o.volume / 100, 0)
})
watchValue(options, o => {
appViewer.inWorldRenderingConfig.vrSupport = o.vrSupport
appViewer.inWorldRenderingConfig.vrPageGameRendering = o.vrPageGameRendering
@ -98,8 +93,6 @@ export const watchOptionsAfterViewerInit = () => {
appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor
appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading
appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks
setSkinsConfig({ apiEnabled: o.loadPlayerSkins })
})
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
@ -120,11 +113,19 @@ export const watchOptionsAfterViewerInit = () => {
})
watchValue(options, o => {
appViewer.inWorldRenderingConfig.defaultSkybox = o.defaultSkybox
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
})
watchValue(options, o => {
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
appViewer.inWorldRenderingConfig.autoLowerRenderDistance = o.autoLowerRenderDistance
})
// Instanced rendering options
watchValue(options, o => {
appViewer.inWorldRenderingConfig.useInstancedRendering = o.useInstancedRendering
appViewer.inWorldRenderingConfig.forceInstancedOnly = o.forceInstancedOnly
appViewer.inWorldRenderingConfig.instancedOnlyDistance = o.instancedOnlyDistance
appViewer.inWorldRenderingConfig.enableSingleColorMode = o.enableSingleColorMode
})
}
@ -135,6 +136,5 @@ export const watchOptionsAfterWorldViewInit = (worldView: WorldDataEmitter) => {
appViewer.inWorldRenderingConfig.renderEars = o.renderEars
appViewer.inWorldRenderingConfig.showHand = o.showHand
appViewer.inWorldRenderingConfig.viewBobbing = o.viewBobbing
appViewer.inWorldRenderingConfig.dayCycle = o.dayCycleAndLighting
})
}