Compare commits
32 commits
next
...
instancing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3aad71024 |
||
|
|
6324fe50b7 |
||
|
|
c63b6bb536 | ||
|
|
c93b7a6f8b | ||
|
|
be0993a00b | ||
|
|
3e5c036512 | ||
|
|
370f07712b | ||
|
|
0921b40f88 | ||
|
|
96c5ebb379 | ||
|
|
2f49cbb35b | ||
|
|
9ee28ef62f | ||
|
|
0240a752ad | ||
|
|
1c8799242a | ||
|
|
b2257d8ae4 | ||
|
|
5e30a4736e | ||
|
|
83018cd828 | ||
|
|
6868068705 | ||
|
|
dbbe5445d8 | ||
|
|
336aad678b | ||
|
|
3c358c9d22 | ||
|
|
7b06561fc7 | ||
|
|
9f29491b5d | ||
|
|
dbce9e7bec | ||
|
|
6083416943 | ||
|
|
102520233a | ||
|
|
a19d459e8a | ||
|
|
561a18527f | ||
|
|
7ec9d10787 | ||
|
|
7a692ac210 | ||
|
|
52a90ce8ff | ||
|
|
4aebfecf69 | ||
|
|
97f8061b06 |
90 changed files with 2982 additions and 4312 deletions
|
|
@ -23,7 +23,6 @@
|
|||
// ],
|
||||
"@stylistic/arrow-spacing": "error",
|
||||
"@stylistic/block-spacing": "error",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@stylistic/brace-style": [
|
||||
"error",
|
||||
"1tbs",
|
||||
|
|
|
|||
2
.github/workflows/benchmark.yml
vendored
2
.github/workflows/benchmark.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
|
||||
[](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
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
@ -10,10 +10,6 @@
|
|||
{
|
||||
"ip": "wss://play.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play.webmc.fun",
|
||||
"name": "WebMC"
|
||||
},
|
||||
{
|
||||
"ip": "wss://ws.fuchsmc.net"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<script type="module" src="three-labels.ts"></script>
|
||||
<style>
|
||||
body { margin: 0; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
242
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
7
renderer/viewer/lib/mesher/instancingUtils.ts
Normal file
7
renderer/viewer/lib/mesher/instancingUtils.ts
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
743
renderer/viewer/three/chunkMeshManager.ts
Normal file
743
renderer/viewer/three/chunkMeshManager.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
30
renderer/viewer/three/getPreflatBlock.ts
Normal file
30
renderer/viewer/three/getPreflatBlock.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
860
renderer/viewer/three/instancedRenderer.ts
Normal file
860
renderer/viewer/three/instancedRenderer.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -504,7 +504,9 @@ const alwaysPressedHandledCommand = (command: Command) => {
|
|||
lockUrl()
|
||||
}
|
||||
if (command === 'communication.toggleMicrophone') {
|
||||
toggleMicrophoneMuted?.()
|
||||
if (typeof toggleMicrophoneMuted === 'function') {
|
||||
toggleMicrophoneMuted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
46
src/dayCycle.ts
Normal 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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
src/index.ts
25
src/index.ts
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ export const startLocalReplayServer = (contents: string) => {
|
|||
const server = createServer({
|
||||
Server: LocalServer as any,
|
||||
version: header.minecraftVersion,
|
||||
keepAlive: false,
|
||||
'online-mode': false
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
||||
|
|
|
|||
|
|
@ -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()} />}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue