Compare commits

...
Sign in to create a new pull request.

35 commits

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

View file

@ -17,9 +17,10 @@ For building the project yourself / contributing, see [Development, Debugging &
### Big Features
- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
- Combined Lighting System - Server Parsing + Client Side Engine for block updates
- Official Mineflayer [plugin integration](https://github.com/zardoy/mcraft-fun-mineflayer-plugin)! View / Control your bot remotely.
- Open any zip world file or even folder in read-write mode!
- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
- Integrated JS server clone capable of opening Java world saves in any way (folders, zip, web chunks streaming, etc)
- Singleplayer mode with simple world generations!
- Works offline

View file

@ -155,6 +155,7 @@
"https-browserify": "^1.0.0",
"mc-assets": "^0.2.62",
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
"minecraft-lighting": "^0.0.10",
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
"mineflayer-mouse": "^0.1.11",
"mineflayer-pathfinder": "^2.4.4",

11
pnpm-lock.yaml generated
View file

@ -339,6 +339,9 @@ importers:
minecraft-inventory-gui:
specifier: github:zardoy/minecraft-inventory-gui#next
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1)
minecraft-lighting:
specifier: ^0.0.10
version: 0.0.10
mineflayer:
specifier: github:zardoy/mineflayer#gen-the-master
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13)
@ -6651,6 +6654,10 @@ packages:
resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41}
version: 1.0.1
minecraft-lighting@0.0.10:
resolution: {integrity: sha512-m3RNe5opaibquxyO0ly1FpKdehapvp9hRRY37RccKY4bio2LGnN3nCZ3PrOXy0C596YpxBsG1OCYg0dqtPzehg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176:
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176}
version: 1.58.0
@ -17279,6 +17286,10 @@ snapshots:
- '@types/react'
- react
minecraft-lighting@0.0.10:
dependencies:
vec3: 0.1.10
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13):
dependencies:
'@types/node-rsa': 1.1.4

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,8 @@ import { proxy } from 'valtio'
import TypedEmitter from 'typed-emitter'
import { delayedIterator } from '../../playground/shared'
import { chunkPos } from './simpleUtils'
import { createLightEngineIfNeededNew, destroyLightEngine, lightRemoveColumn, processLightChunk, updateBlockLight } from './lightEngine'
import { WorldRendererConfig } from './worldrendererCommon'
export type ChunkPosKey = string // like '16,16'
type ChunkPos = { x: number, z: number } // like { x: 16, z: 16 }
@ -32,9 +34,19 @@ export type WorldDataEmitterEvents = {
export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
static readonly restorerName = 'WorldDataEmitterWorker'
destroy () {
this.removeAllListeners()
}
}
export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
minY = -64
worldHeight = 384
dimensionName = ''
version = ''
worldRendererConfig: WorldRendererConfig
loadedChunks: Record<ChunkPosKey, boolean>
readonly lastPos: Vec3
private eventListeners: Record<string, any> = {}
@ -64,18 +76,22 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
this.emitter = this
}
setBlockStateId (position: Vec3, stateId: number) {
const val = this.world.setBlockStateId(position, stateId) as Promise<void> | void
if (val) throw new Error('setBlockStateId returned promise (not supported)')
// const chunkX = Math.floor(position.x / 16)
// const chunkZ = Math.floor(position.z / 16)
// if (!this.loadedChunks[`${chunkX},${chunkZ}`] && !this.waitingSpiralChunksLoad[`${chunkX},${chunkZ}`]) {
// void this.loadChunk({ x: chunkX, z: chunkZ })
// return
// }
// setBlockStateId (position: Vec3, stateId: number) {
// const val = this.world.setBlockStateId(position, stateId) as Promise<void> | void
// if (val) throw new Error('setBlockStateId returned promise (not supported)')
// // const chunkX = Math.floor(position.x / 16)
// // const chunkZ = Math.floor(position.z / 16)
// // if (!this.loadedChunks[`${chunkX},${chunkZ}`] && !this.waitingSpiralChunksLoad[`${chunkX},${chunkZ}`]) {
// // void this.loadChunk({ x: chunkX, z: chunkZ })
// // return
// // }
this.emit('blockUpdate', { pos: position, stateId })
}
// const updateChunks = this.worldRendererConfig.clientSideLighting ? updateBlockLight(position.x, position.y, position.z, stateId) ?? [] : []
// this.emit('blockUpdate', { pos: position, stateId })
// for (const chunk of updateChunks) {
// void this.loadChunk(new Vec3(chunk[0] * 16, 0, chunk[1] * 16), true, 'setBlockStateId light update')
// }
// }
updateViewDistance (viewDistance: number) {
this.viewDistance = viewDistance
@ -83,6 +99,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
}
listenToBot (bot: typeof __type_bot) {
this.version = bot.version
const entitiesObjectData = new Map<string, number>()
bot._client.prependListener('spawn_entity', (data) => {
if (data.objectData && data.entityId !== undefined) {
@ -143,9 +160,16 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
chunkColumnUnload: (pos: Vec3) => {
this.unloadChunk(pos)
},
blockUpdate: (oldBlock: any, newBlock: any) => {
blockUpdate: async (oldBlock, newBlock) => {
if (typeof newBlock.stateId === 'number' && oldBlock?.stateId === newBlock.stateId) return
const stateId = newBlock.stateId ?? ((newBlock.type << 4) | newBlock.metadata)
this.emitter.emit('blockUpdate', { pos: oldBlock.position, stateId })
const distance = newBlock.position.distanceTo(this.lastPos)
this.emit('blockUpdate', { pos: newBlock.position, stateId })
const updateChunks = this.worldRendererConfig.clientSideLighting === 'none' ? [] : await updateBlockLight(newBlock.position.x, newBlock.position.y, newBlock.position.z, stateId, distance) ?? []
for (const chunk of updateChunks) {
void this.loadChunk(new Vec3(chunk.chunkX * 16, 0, chunk.chunkZ * 16), true, 'setBlockStateId light update')
}
},
time: () => {
this.emitter.emit('time', bot.time.timeOfDay)
@ -154,17 +178,22 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
this.emitter.emit('end')
},
// when dimension might change
login: () => {
void this.updatePosition(bot.entity.position, true)
this.emitter.emit('playerEntity', bot.entity)
login () {
possiblyDimensionChange()
},
respawn: () => {
void this.updatePosition(bot.entity.position, true)
this.emitter.emit('playerEntity', bot.entity)
possiblyDimensionChange()
this.emitter.emit('onWorldSwitch')
},
} satisfies Partial<BotEvents>
const possiblyDimensionChange = () => {
this.minY = bot.game['minY'] ?? -64
this.worldHeight = bot.game['height'] ?? 384
this.dimensionName = bot.game['dimension'] ?? ''
void this.updatePosition(bot.entity.position, true)
this.emitter.emit('playerEntity', bot.entity)
}
bot._client.on('update_light', ({ chunkX, chunkZ }) => {
const chunkPos = new Vec3(chunkX * 16, 0, chunkZ * 16)
@ -204,6 +233,14 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
}
}
destroy () {
if (bot) {
this.removeListenersFromBot(bot as any)
}
this.emitter.removeAllListeners()
destroyLightEngine()
}
async init (pos: Vec3) {
this.updateViewDistance(this.viewDistance)
this.emitter.emit('chunkPosUpdate', { pos })
@ -268,14 +305,33 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
// lastTime = 0
async loadChunk (pos: ChunkPos, isLightUpdate = false, reason = 'spiral') {
const [botX, botZ] = chunkPos(this.lastPos)
createLightEngineIfNeededNew(this, this.version)
const dx = Math.abs(botX - Math.floor(pos.x / 16))
const dz = Math.abs(botZ - Math.floor(pos.z / 16))
const [botX, botZ] = chunkPos(this.lastPos)
const chunkX = Math.floor(pos.x / 16)
const chunkZ = Math.floor(pos.z / 16)
const dx = Math.abs(botX - chunkX)
const dz = Math.abs(botZ - chunkZ)
if (dx <= this.viewDistance && dz <= this.viewDistance) {
// eslint-disable-next-line @typescript-eslint/await-thenable -- todo allow to use async world provider but not sure if needed
const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z))
if (column) {
let result = [] as Array<{ chunkX: number, chunkZ: number }>
if (!isLightUpdate) {
const computeLighting = this.worldRendererConfig.clientSideLighting === 'full'
const promise = processLightChunk(pos.x, pos.z, computeLighting)
if (computeLighting) {
result = (await promise) ?? []
}
}
if (!result) return
for (const affectedChunk of result) {
if (affectedChunk.chunkX === chunkX && affectedChunk.chunkZ === chunkZ) continue
const loadedChunk = this.loadedChunks[`${affectedChunk.chunkX * 16},${affectedChunk.chunkZ * 16}`]
if (!loadedChunk) continue
void this.loadChunk(new Vec3(affectedChunk.chunkX * 16, 0, affectedChunk.chunkZ * 16), true)
}
// const latency = Math.floor(performance.now() - this.lastTime)
// this.debugGotChunkLatency.push(latency)
// this.lastTime = performance.now()
@ -317,6 +373,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
this.emitter.emit('unloadChunk', { x: pos.x, z: pos.z })
delete this.loadedChunks[`${pos.x},${pos.z}`]
delete this.debugChunksInfo[`${pos.x},${pos.z}`]
lightRemoveColumn(pos.x, pos.z)
}
async updatePosition (pos: Vec3, force = false) {

View file

@ -15,6 +15,7 @@ import { buildCleanupDecorator } from './cleanupDecorator'
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared'
import { chunkPos } from './simpleUtils'
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
import { dumpLightData } from './lightEngine'
import { WorldDataEmitterWorker } from './worldDataEmitter'
import { getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from './basePlayerState'
import { MesherLogReader } from './mesherlogReader'
@ -44,6 +45,9 @@ export const defaultWorldRendererConfig = {
clipWorldBelowY: undefined as number | undefined,
smoothLighting: true,
enableLighting: true,
legacyLighting: false,
clientSideLighting: 'full' as 'full' | 'partial' | 'none',
flyingSquidWorkarounds: false,
starfield: true,
addChunksBatchWaitTime: 200,
vrSupport: true,
@ -62,6 +66,7 @@ export const defaultWorldRendererConfig = {
export type WorldRendererConfig = typeof defaultWorldRendererConfig
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
skyLight = 15
worldReadyResolvers = Promise.withResolvers<void>()
worldReadyPromise = this.worldReadyResolvers.promise
timeOfTheDay = 0
@ -496,6 +501,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
timeUpdated? (newTime: number): void
skylightUpdated? (): void
updateViewerPosition (pos: Vec3) {
this.viewerChunkPosition = pos
for (const [key, value] of Object.entries(this.loadedChunks)) {
@ -543,7 +550,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.sendMesherMcData()
}
getMesherConfig (): MesherConfig {
changeSkyLight () {
let skyLight = 15
const timeOfDay = this.timeOfTheDay
if (timeOfDay < 0 || timeOfDay > 24_000) {
@ -556,34 +563,35 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
skyLight = ((timeOfDay - 12_000) / 6000) * 15
}
skyLight = Math.floor(skyLight)
this.skyLight = Math.floor(skyLight)
}
getMesherConfig (): MesherConfig {
return {
version: this.version,
enableLighting: this.worldRendererConfig.enableLighting,
skyLight,
enableLighting: this.worldRendererConfig.enableLighting && !this.playerStateReactive.lightingDisabled,
skyLight: this.skyLight,
smoothLighting: this.worldRendererConfig.smoothLighting,
outputFormat: this.outputFormat,
// textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
debugModelVariant: undefined,
clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY,
disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers,
usingCustomLightHolder: false,
flyingSquidWorkarounds: this.worldRendererConfig.flyingSquidWorkarounds,
worldMinY: this.worldMinYRender,
worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight,
}
}
sendMesherMcData () {
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajorVersion(this.version)]
const mcData = {
version: JSON.parse(JSON.stringify(allMcData.version))
}
for (const key of dynamicMcDataFiles) {
mcData[key] = allMcData[key]
}
for (const worker of this.workers) {
worker.postMessage({ type: 'mcData', mcData, config: this.getMesherConfig() })
}
meshersSendMcData(
this.workers,
this.version,
{
config: this.getMesherConfig()
}
)
this.logWorkerWork('# mcData sent')
}
@ -641,7 +649,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
x,
z,
chunk,
customBlockModels: customBlockModels || undefined
customBlockModels: customBlockModels || undefined,
lightData: dumpLightData(x, z)
})
}
this.workers[0].postMessage({
@ -817,19 +826,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
})
worldEmitter.on('time', (timeOfDay) => {
this.timeUpdated?.(timeOfDay)
if (timeOfDay < 0 || timeOfDay > 24_000) {
throw new Error('Invalid time of day. It should be between 0 and 24000.')
}
const oldSkyLight = this.skyLight
this.timeOfTheDay = timeOfDay
// if (this.worldRendererConfig.skyLight === skyLight) return
// this.worldRendererConfig.skyLight = skyLight
// if (this instanceof WorldRendererThree) {
// (this).rerenderAllChunks?.()
// }
this.changeSkyLight()
if (oldSkyLight !== this.skyLight) {
this.skylightUpdated?.()
}
this.timeUpdated?.(timeOfDay)
})
}
@ -922,7 +929,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.reactiveState.world.mesherWork = true
const distance = this.getDistance(pos)
// todo shouldnt we check loadedChunks instead?
if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
// if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
// if (this.sectionsOutstanding.has(key)) return
this.renderUpdateEmitter.emit('dirty', pos, value)
@ -1026,12 +1033,16 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.renderUpdateEmitter.removeAllListeners()
this.abortController.abort()
removeAllStats()
this.displayOptions.worldView.destroy()
}
}
export const initMesherWorker = (onGotMessage: (data: any) => void) => {
// Node environment needs an absolute path, but browser needs the url of the file
const workerName = 'mesher.js'
// eslint-disable-next-line node/no-path-concat
const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName
let worker: any
if (process.env.SINGLE_FILE_BUILD) {
@ -1039,7 +1050,7 @@ export const initMesherWorker = (onGotMessage: (data: any) => void) => {
const blob = new Blob([workerCode], { type: 'text/javascript' })
worker = new Worker(window.URL.createObjectURL(blob))
} else {
worker = new Worker(workerName)
worker = new Worker(src)
}
worker.onmessage = ({ data }) => {

View file

@ -28,7 +28,7 @@ type SectionKey = string
export class WorldRendererThree extends WorldRendererCommon {
outputFormat = 'threeJs' as const
sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean }> = {}
sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean, hasSkylight?: boolean }> = {}
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
signsCache = new Map<string, any>()
starField: StarField
@ -165,11 +165,19 @@ export class WorldRendererThree extends WorldRendererCommon {
})
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
if (!value) return
this.ambientLight.intensity = value
if (this.worldRendererConfig.legacyLighting) {
this.ambientLight.intensity = value
} else {
this.ambientLight.intensity = 1
}
})
this.onReactivePlayerStateUpdated('directionalLight', (value) => {
if (!value) return
this.directionalLight.intensity = value
if (this.worldRendererConfig.legacyLighting) {
this.directionalLight.intensity = value
} else {
this.directionalLight.intensity = 0.4
}
})
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
@ -254,10 +262,38 @@ export class WorldRendererThree extends WorldRendererCommon {
}
}
skylightUpdated (): void {
let updated = 0
for (const sectionKey of Object.keys(this.sectionObjects)) {
if (this.sectionObjects[sectionKey].hasSkylight) {
// set section to be updated
const [x, y, z] = sectionKey.split(',').map(Number)
this.setSectionDirty(new Vec3(x, y, z))
updated++
}
}
console.log(`Skylight changed to ${this.skyLight}. Updated`, updated, 'sections')
}
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
return getItemUv(item, specificProps, this.resourcesManager, this.playerStateReactive)
}
debugOnlySunlightSections (enable: boolean, state = true) {
for (const sectionKey of Object.keys(this.sectionObjects)) {
if (!enable) {
this.sectionObjects[sectionKey].visible = true
continue
}
if (this.sectionObjects[sectionKey].hasSkylight) {
this.sectionObjects[sectionKey].visible = state
} else {
this.sectionObjects[sectionKey].visible = false
}
}
}
async demoModel () {
//@ts-expect-error
const pos = cursorBlockRel(0, 1, 0).position
@ -345,7 +381,7 @@ export class WorldRendererThree extends WorldRendererCommon {
// debugRecomputedDeletedObjects = 0
handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void {
if (data.type !== 'geometry') return
let object: THREE.Object3D = this.sectionObjects[data.key]
let object = this.sectionObjects[data.key]
if (object) {
this.scene.remove(object)
disposeObject(object)
@ -404,7 +440,10 @@ export class WorldRendererThree extends WorldRendererCommon {
object.add(head)
}
}
object.hasSkylight = data.geometry.hasSkylight
this.sectionObjects[data.key] = object
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
object.visible = false
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`

View file

@ -197,6 +197,7 @@ 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.worldRendererConfig = this.inWorldRenderingConfig
window.worldView = this.worldView
watchOptionsAfterWorldViewInit(this.worldView)
this.appConfigUdpate()
@ -260,6 +261,7 @@ export class AppViewer {
if (cleanState) {
this.currentState = undefined
this.currentDisplay = null
this.worldView?.destroy()
this.worldView = undefined
}
if (this.backend) {

View file

@ -10,7 +10,7 @@ export default () => {
const night = 13_500
const morningStart = 23_000
const morningEnd = 23_961
const timeProgress = options.dayCycleAndLighting ? bot.time.timeOfDay : 0
const timeProgress = options.dayCycle ? bot.time.timeOfDay : 0
// todo check actual colors
const dayColorRainy = { r: 111 / 255, g: 156 / 255, b: 236 / 255 }
@ -35,10 +35,10 @@ export default () => {
// 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)
}
// if (!options.newVersionsLighting && bot.supportFeature('blockStateId')) {
// appViewer.playerState.reactive.ambientLight = Math.max(int, 0.25)
// appViewer.playerState.reactive.directionalLight = Math.min(int, 0.45)
// }
}
bot.on('time', timeUpdated)

View file

@ -36,7 +36,7 @@ export const defaultOptions = {
/** @unstable */
debugLogNotFrequentPackets: false,
unimplementedContainers: false,
dayCycleAndLighting: true,
dayCycle: true,
loadPlayerSkins: true,
renderEars: true,
lowMemoryMode: false,
@ -92,8 +92,16 @@ export const defaultOptions = {
showCursorBlockInSpectator: false,
renderEntities: true,
smoothLighting: true,
newVersionsLighting: false,
chatSelect: true,
// experimentalLighting: IS_BETA_TESTER,
experimentalLightingV1: false,
/**
* Controls how lighting is calculated and rendered:
* - 'always-client': Always use client-side lighting engine for all light calculations
* - 'prefer-server': Use server lighting data when available, fallback to client-side calculations
* - 'always-server': Only use lighting data from the server, disable client-side calculations
*/
lightingStrategy: 'prefer-server' as 'always-client' | 'prefer-server' | 'always-server',
autoJump: 'auto' as 'auto' | 'always' | 'never',
autoParkour: false,
vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users

View file

@ -82,19 +82,23 @@ export const guiOptionsScheme: {
custom () {
return <Category>Experimental</Category>
},
dayCycleAndLighting: {
text: 'Day Cycle',
experimentalLightingV1: {
text: 'Experimental Lighting',
tooltip: 'Once stable this setting will be removed and always enabled',
},
smoothLighting: {},
newVersionsLighting: {
text: 'Lighting in Newer Versions',
lightingStrategy: {
values: [
['prefer-server', 'Prefer Server'],
['always-client', 'Always Client'],
['always-server', 'Always Server'],
],
},
lowMemoryMode: {
text: 'Low Memory Mode',
enableWarning: 'Enabling it will make chunks load ~4x slower. When in the game, app needs to be reloaded to apply this setting.',
},
starfieldRendering: {},
renderEntities: {},
keepChunksDistance: {
max: 5,
unit: '',

View file

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

View file

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

View file

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

View file

@ -100,12 +100,40 @@ export const watchOptionsAfterViewerInit = () => {
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
})
subscribeKey(options, 'newVersionsLighting', () => {
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
})
const updateLightingStrategy = () => {
if (!bot) return
if (!options.experimentalLightingV1) {
appViewer.inWorldRenderingConfig.clientSideLighting = 'none'
appViewer.inWorldRenderingConfig.enableLighting = false
appViewer.inWorldRenderingConfig.legacyLighting = true
return
}
const lightingEnabled = options.dayCycle
if (!lightingEnabled) {
appViewer.inWorldRenderingConfig.clientSideLighting = 'none'
appViewer.inWorldRenderingConfig.enableLighting = false
return
}
appViewer.inWorldRenderingConfig.legacyLighting = false
// for now ignore saved lighting to allow proper updates and singleplayer created worlds
// appViewer.inWorldRenderingConfig.flyingSquidWorkarounds = miscUiState.flyingSquid
const serverParsingSupported = miscUiState.flyingSquid ? /* !bot.supportFeature('blockStateId') */false : bot.supportFeature('blockStateId')
const serverLightingPossible = serverParsingSupported && (options.lightingStrategy === 'prefer-server' || options.lightingStrategy === 'always-server')
const clientLightingPossible = options.lightingStrategy !== 'always-server'
const clientSideLighting = !serverLightingPossible
appViewer.inWorldRenderingConfig.clientSideLighting = serverLightingPossible && clientLightingPossible ? 'partial' : clientSideLighting ? 'full' : 'none'
appViewer.inWorldRenderingConfig.enableLighting = serverLightingPossible || clientLightingPossible
}
subscribeKey(options, 'lightingStrategy', updateLightingStrategy)
customEvents.on('mineflayerBotCreated', () => {
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
updateLightingStrategy()
})
watchValue(options, o => {