diff --git a/README.MD b/README.MD index 61a5b7333..1f91c58ed 100644 --- a/README.MD +++ b/README.MD @@ -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 diff --git a/package.json b/package.json index fe4adb162..b3e8b1dd6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c37e0510b..82c4c7c17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/renderer/viewer/lib/lightEngine.ts b/renderer/viewer/lib/lightEngine.ts new file mode 100644 index 000000000..35bb1e471 --- /dev/null +++ b/renderer/viewer/lib/lightEngine.ts @@ -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 | 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 + // 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 +} diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index a063d77fe..4ad7196ce 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -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) diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index aca47e150..905ece07c 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -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 } diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index 230db6b92..2e6f3f1f9 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -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 { diff --git a/renderer/viewer/lib/mesher/world.ts b/renderer/viewer/lib/mesher/world.ts index f2757ae62..675ceca0a 100644 --- a/renderer/viewer/lib/mesher/world.ts +++ b/renderer/viewer/lib/mesher/world.ts @@ -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 & { } 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 diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts index e9153af74..5edb9dc48 100644 --- a/renderer/viewer/lib/worldDataEmitter.ts +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -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) { static readonly restorerName = 'WorldDataEmitterWorker' + + destroy () { + this.removeAllListeners() + } } export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter) { + minY = -64 + worldHeight = 384 + dimensionName = '' + version = '' + + worldRendererConfig: WorldRendererConfig loadedChunks: Record readonly lastPos: Vec3 private eventListeners: Record = {} @@ -64,18 +76,22 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter | 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 }) - } + // setBlockStateId (position: Vec3, stateId: number) { + // const val = this.world.setBlockStateId(position, stateId) as Promise | 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 + // // } + + // 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() bot._client.prependListener('spawn_entity', (data) => { if (data.objectData && data.entityId !== undefined) { @@ -143,9 +160,16 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter { 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 { - 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 + 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 TypedEmitter + 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 { + skyLight = 15 worldReadyResolvers = Promise.withResolvers() worldReadyPromise = this.worldReadyResolvers.promise timeOfTheDay = 0 @@ -496,6 +501,8 @@ export abstract class WorldRendererCommon 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 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 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 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 }) 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 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 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 }) => { diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index bc95f06b8..96295f269 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -28,7 +28,7 @@ type SectionKey = string export class WorldRendererThree extends WorldRendererCommon { outputFormat = 'threeJs' as const - sectionObjects: Record = {} + sectionObjects: Record = {} chunkTextures = new Map() signsCache = new Map() 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, 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]}` diff --git a/src/appViewer.ts b/src/appViewer.ts index bf01b989b..47201b696 100644 --- a/src/appViewer.ts +++ b/src/appViewer.ts @@ -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) { diff --git a/src/dayCycle.ts b/src/dayCycle.ts index 50e63a21b..9e3c731fe 100644 --- a/src/dayCycle.ts +++ b/src/dayCycle.ts @@ -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) diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index 9650ffcd6..b146b7cd7 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -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 diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index b03db37d7..b1717deee 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -82,19 +82,23 @@ export const guiOptionsScheme: { custom () { return Experimental }, - 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: '', diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 22d5ef26d..12c640f5b 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -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>) => { + if (options.dayCycleAndLighting) { + delete options.dayCycleAndLighting + options.dayCycle = options.dayCycleAndLighting + } + if (options.highPerformanceGpu) { options.gpuPreference = 'high-performance' delete options.highPerformanceGpu diff --git a/src/react/DebugOverlay.tsx b/src/react/DebugOverlay.tsx index 8e42fd38d..a6bff857b 100644 --- a/src/react/DebugOverlay.tsx +++ b/src/react/DebugOverlay.tsx @@ -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 () => {

Client TPS: {clientTps} {serverTps ? `Server TPS: ${serverTps.value} ${serverTps.frozen ? '(frozen)' : ''}` : ''}

Facing (viewer): {bot.entity.yaw.toFixed(3)} {bot.entity.pitch.toFixed(3)}

Facing (minecraft): {quadsDescription[minecraftQuad.current]} ({minecraftYaw.current.toFixed(1)} {(bot.entity.pitch * -180 / Math.PI).toFixed(1)})

-

Light: {blockL} ({skyL} sky)

+

Light: {lightInfo.block} ({lightInfo.sky} sky) ({lightInfo.info})

Biome: minecraft:{loadedData.biomesArray[biomeId]?.name ?? 'unknown biome'}

Day: {day} Time: {timeOfDay}

diff --git a/src/shims/minecraftData.ts b/src/shims/minecraftData.ts index 33dff5fef..5060dea69 100644 --- a/src/shims/minecraftData.ts +++ b/src/shims/minecraftData.ts @@ -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 diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 478da4fb1..ae16134ec 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -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 => {