Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
245300f
init
zardoy Feb 18, 2025
d5c61d8
a working light
zardoy Feb 19, 2025
48ead54
should work.
zardoy Mar 12, 2025
037e297
Merge remote-tracking branch 'origin/next' into light-engine
zardoy Mar 12, 2025
ace45a9
not crash pls
zardoy Mar 13, 2025
ec6b249
Merge remote-tracking branch 'origin/next' into light-engine
zardoy Mar 21, 2025
9f505f8
rm workaround
zardoy Mar 21, 2025
f18b3a1
Merge branch 'next' into light-engine
zardoy Apr 7, 2025
e10f610
humble and terrible progress
zardoy Apr 10, 2025
0fa66e2
Merge remote-tracking branch 'origin/next' into light-engine
zardoy Apr 10, 2025
b1ba2cd
Merge remote-tracking branch 'origin/next' into light-engine
zardoy Apr 12, 2025
3cd1ac3
Merge branch 'next' into light-engine
zardoy Apr 24, 2025
1918c68
finish lighting
zardoy Apr 25, 2025
b4c72db
fix crash opt
zardoy Apr 25, 2025
1f5b682
FINISH OPTIONS, FINISH RECOMPUTE, ADD LIGHT TO WATER
zardoy Apr 28, 2025
5a57d29
Merge branch 'next' into light-engine
zardoy Apr 28, 2025
2f6191a
Merge remote-tracking branch 'origin/next' into light-engine
zardoy Apr 30, 2025
f4eab39
finish lighting
zardoy Apr 30, 2025
79f0fdd
fix lava rendering
zardoy Apr 30, 2025
27c55b1
finish!
zardoy Apr 30, 2025
f4f5edd
fix lighting disabling
zardoy Apr 30, 2025
c97c7e0
finish combined computation, finish settings and strategies
zardoy May 1, 2025
c4b9c33
Update src/optionsStorage.ts
zardoy May 1, 2025
7d224fb
Merge remote-tracking branch 'origin/next' into light-engine
zardoy May 1, 2025
d6f394f
hide cursor block in spectator
zardoy May 2, 2025
ddf0810
final step: move engine to another thread
zardoy May 4, 2025
5720cfa
up light
zardoy May 4, 2025
7dba526
Merge remote-tracking branch 'origin/next' into light-engine
zardoy May 4, 2025
e95f84e
fix lock
zardoy May 4, 2025
90de0d0
up chunk?
zardoy May 4, 2025
f185df9
fix remaining issues with worker bundle with smart approach
zardoy May 8, 2025
6be3c5c
Merge remote-tracking branch 'origin/next' into light-engine
zardoy May 8, 2025
56aee16
Merge branch 'next' into light-engine
zardoy May 19, 2025
b8c8f8a
Merge branch 'next' into light-engine
zardoy Jul 14, 2025
6a8d15b
[deploy] properly destroy world view
zardoy Jul 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@
"timers-browserify": "^2.0.12",
"typescript": "5.5.4",
"vitest": "^0.34.6",
"yaml": "^2.3.2"
"yaml": "^2.3.2",
"minecraft-lighting": "^0.0.8"
},
"optionalDependencies": {
"cypress": "^10.11.0",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions renderer/viewer/lib/lightEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { LightWorld, createLightEngineForSyncWorld, convertPrismarineBlockToWorldBlock } from 'minecraft-lighting'
import { world } from 'prismarine-world'
import { WorldRendererCommon } from './worldrendererCommon'

let lightEngine: LightWorld | null = null
export const getLightEngine = () => {
if (!lightEngine) throw new Error('Light engine not initialized')
return lightEngine
}
export const getLightEngineSafe = () => {
return lightEngine
}

export const createLightEngine = (world: WorldRendererCommon) => {
lightEngine = createLightEngineForSyncWorld(world.displayOptions.worldView.world as unknown as world.WorldSync, loadedData, {
minY: world.worldSizeParams.minY,
height: world.worldSizeParams.worldHeight,
// enableSkyLight: false,
})
lightEngine.PARALLEL_CHUNK_PROCESSING = false
globalThis.lightEngine = lightEngine
}

export const processLightChunk = async (x: number, z: number) => {
const chunkX = Math.floor(x / 16)
const chunkZ = Math.floor(z / 16)
const engine = getLightEngine()
// fillColumnWithZeroLight(engine.externalWorld, chunkX, chunkZ)

const updated = engine.receiveUpdateColumn(chunkX, chunkZ)
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 updateBlockLight = (x: number, y: number, z: number, stateId: number) => {
const engine = getLightEngine()
engine.setBlock(x, y, z, convertPrismarineBlockToWorldBlock(mcData.blocks[stateId], loadedData))
}
5 changes: 5 additions & 0 deletions renderer/viewer/lib/mesher/mesher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ const handleMessage = data => {
}
case 'chunk': {
world.addColumn(data.x, data.z, data.chunk)
if (data.lightData) {
world.lightHolder.loadChunk(data.lightData)
} else {
console.warn('no light data', data.x, data.z)
}
if (data.customBlockModels) {
const chunkKey = `${data.x},${data.z}`
world.customBlockModels.set(chunkKey, data.customBlockModels)
Expand Down
58 changes: 39 additions & 19 deletions renderer/viewer/lib/mesher/world.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -32,6 +33,7 @@ export type WorldBlock = Omit<Block, 'position'> & {
}

export class World {
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 }
Expand All @@ -51,6 +53,7 @@ export class World {
}

getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') {
const IS_USING_SERVER_LIGHTING = false
// for easier testing
if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
const { enableLighting, skyLight } = this.config
Expand All @@ -59,32 +62,49 @@ export class World {
// 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
let result = Math.max(
3,
Math.min(
15,
Math.max(
this.getBlockLight(pos),
Math.min(skyLight, this.getSkyLight(pos))
)
)
)
const MIN_LIGHT_LEVEL = 2
result = Math.max(result, MIN_LIGHT_LEVEL)
// 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_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) {
return this.lightHolder.getBlockLight(pos.x, pos.y, pos.z)
// return column.getBlockLight(posInChunk(pos))
}

getSkyLight (pos: Vec3) {
return this.lightHolder.getSkyLight(pos.x, pos.y, pos.z)
// return column.getSkyLight(posInChunk(pos))
}

addColumn (x, z, json) {
const chunk = this.Chunk.fromJson(json)
this.columns[columnKey(x, z)] = chunk as any
Expand Down
18 changes: 14 additions & 4 deletions renderer/viewer/lib/worldDataEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import { Vec3 } from 'vec3'
import { BotEvents } from 'mineflayer'
import { proxy } from 'valtio'
import TypedEmitter from 'typed-emitter'
import { getItemFromBlock } from '../../../src/chatUtils'
import { delayedIterator } from '../../playground/shared'
import { playerState } from '../../../src/mineflayer/playerState'
import { chunkPos } from './simpleUtils'
import { processLightChunk, updateBlockLight } from './lightEngine'

export type ChunkPosKey = string // like '16,16'
type ChunkPos = { x: number, z: number } // like { x: 16, z: 16 }
Expand Down Expand Up @@ -81,6 +80,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
// return
// }

updateBlockLight(position.x, position.y, position.z, stateId)
this.emit('blockUpdate', { pos: position, stateId })
}

Expand Down Expand Up @@ -252,13 +252,23 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo

async loadChunk (pos: ChunkPos, isLightUpdate = false, reason = 'spiral') {
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 - Math.floor(pos.x / 16))
const dz = Math.abs(botZ - 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) {
const result = await processLightChunk(pos.x, pos.z)
if (!result) return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential logic issue with null/empty array check

The check if (!result) return may not work as expected since an empty array is truthy in JavaScript.

If you're trying to check for an empty array, use if (!result.length) return or for a null/undefined check, use if (result == null) return.

-if (!result) return
+if (!result || !result.length) return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!result) return
// around line 320 in renderer/viewer/lib/worldDataEmitter.ts
- if (!result) return
+ if (!result || !result.length) return

for (const affectedChunk of result) {
if (affectedChunk.x === chunkX && affectedChunk.z === chunkZ) continue
const loadedChunk = this.loadedChunks[`${affectedChunk.x},${affectedChunk.z}`]
if (!loadedChunk) continue
void this.loadChunk(new Vec3(affectedChunk.x * 16, 0, affectedChunk.z * 16), true)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider adding safeguards for recursive light propagation

The implementation correctly processes light updates and propagates them to affected neighboring chunks. However, if chunks affect each other in a circular pattern, this could potentially lead to excessive recursion.

Consider adding a recursion depth counter or tracking processed chunks to prevent potential infinite loops:

async loadChunk (pos: ChunkPos, isLightUpdate = false, recursionDepth = 0) {
  // existing code...
  
  const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z))
  if (column) {
    const result = await processLightChunk(pos.x, pos.z)
    if (!result) return
+   // Prevent excessive recursion
+   if (recursionDepth >= 5) return
    
    for (const affectedChunk of result) {
      if (affectedChunk.x === chunkX && affectedChunk.z === chunkZ) continue
      const loadedChunk = this.loadedChunks[`${affectedChunk.x},${affectedChunk.z}`]
      if (!loadedChunk) continue
-     void this.loadChunk(new Vec3(affectedChunk.x * 16, 0, affectedChunk.z * 16), true)
+     void this.loadChunk(new Vec3(affectedChunk.x * 16, 0, affectedChunk.z * 16), true, recursionDepth + 1)
    }
    
    // rest of the existing code...
}

Let's verify if there are any similar safeguards in the current implementation:


🏁 Script executed:

#!/bin/bash
# Check if there's any existing recursion prevention in light processing

# Look for depth tracking or visited chunks in light-related functions
rg -A 3 -B 3 "recursion|depth|visited|processed" --glob "*.ts" 

# Look for any maps/sets used to track processed chunks in light engine
ast-grep --pattern $'const $_ = new (Set|Map)<$_>'

Length of output: 41150


Action Required: Add Safeguards for Recursive Light Propagation

Our verification did not uncover any existing safeguard for recursion in light processing within renderer/viewer/lib/worldDataEmitter.ts. In its current form, the logic propagates light updates by recursively calling loadChunk without any check to prevent infinite recursion when chunks create circular dependencies.

Please consider introducing a recursion depth counter (or a mechanism to track already processed chunks) to cap recursive calls. For example, a diff similar to the one suggested below would add a guard that stops further recursion after a fixed depth (e.g., 5), making the propagation robust against potential infinite loops:

async loadChunk (pos: ChunkPos, isLightUpdate = false, recursionDepth = 0) {
  // existing code...
  
  const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z))
  if (column) {
    const result = await processLightChunk(pos.x, pos.z)
    if (!result) return
+   // Prevent excessive recursion by capping the recursion depth
+   if (recursionDepth >= 5) return
    
    for (const affectedChunk of result) {
      if (affectedChunk.x === chunkX && affectedChunk.z === chunkZ) continue
      const loadedChunk = this.loadedChunks[`${affectedChunk.x},${affectedChunk.z}`]
      if (!loadedChunk) continue
-     void this.loadChunk(new Vec3(affectedChunk.x * 16, 0, affectedChunk.z * 16), true)
+     void this.loadChunk(new Vec3(affectedChunk.x * 16, 0, affectedChunk.z * 16), true, recursionDepth + 1)
    }
    
    // rest of the existing code...
  }
}

Implementing this safeguard should mitigate the risk of infinite recursion during light propagation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = await processLightChunk(pos.x, pos.z)
if (!result) return
for (const affectedChunk of result) {
if (affectedChunk.x === chunkX && affectedChunk.z === chunkZ) continue
const loadedChunk = this.loadedChunks[`${affectedChunk.x},${affectedChunk.z}`]
if (!loadedChunk) continue
void this.loadChunk(new Vec3(affectedChunk.x * 16, 0, affectedChunk.z * 16), true)
}
async loadChunk (pos: ChunkPos, isLightUpdate = false, recursionDepth = 0) {
// existing code...
const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z))
if (column) {
const result = await processLightChunk(pos.x, pos.z)
if (!result) return
// Prevent excessive recursion by capping the recursion depth
if (recursionDepth >= 5) return
for (const affectedChunk of result) {
if (affectedChunk.x === chunkX && affectedChunk.z === chunkZ) continue
const loadedChunk = this.loadedChunks[`${affectedChunk.x},${affectedChunk.z}`]
if (!loadedChunk) continue
void this.loadChunk(new Vec3(affectedChunk.x * 16, 0, affectedChunk.z * 16), true, recursionDepth + 1)
}
// rest of the existing code...
}
}

// const latency = Math.floor(performance.now() - this.lastTime)
// this.debugGotChunkLatency.push(latency)
// this.lastTime = performance.now()
Expand Down
7 changes: 6 additions & 1 deletion renderer/viewer/lib/worldrendererCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { chunkPos } from './simpleUtils'
import { addNewStat, removeAllStats, removeStat, updatePanesVisibility, updateStatText } from './ui/newStats'
import { WorldDataEmitter } from './worldDataEmitter'
import { IPlayerState } from './basePlayerState'
import { createLightEngine, dumpLightData, getLightEngine, getLightEngineSafe } from './lightEngine'
import { MesherLogReader } from './mesherlogReader'

function mod (x, n) {
Expand All @@ -39,6 +40,7 @@ export const defaultWorldRendererConfig = {
clipWorldBelowY: undefined as number | undefined,
smoothLighting: true,
enableLighting: true,
clientSideLighting: false,
starfield: true,
addChunksBatchWaitTime: 200,
vrSupport: true,
Expand Down Expand Up @@ -169,6 +171,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}

constructor (public readonly resourcesManager: ResourcesManager, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) {
createLightEngine(this)

// this.initWorkers(1) // preload script on page load
this.snapshotInitialValues()
this.worldRendererConfig = displayOptions.inWorldRenderingConfig
Expand Down Expand Up @@ -610,7 +614,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
x,
z,
chunk,
customBlockModels: customBlockModels || undefined
customBlockModels: customBlockModels || undefined,
lightData: dumpLightData(x, z)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix commented-out light data dumping.

The dumpLightData function is currently not returning any data because its return statement is commented out, as seen in the relevant code snippet from lightEngine.ts. This will result in light data always being undefined.

#!/bin/bash
# Verify the implementation of dumpLightData function

echo "Current dumpLightData implementation:"
rg -A 5 "dumpLightData" --glob "*.{ts,tsx,js,jsx}"
🤖 Prompt for AI Agents
In renderer/viewer/lib/worldrendererCommon.ts at line 627, the call to
dumpLightData(x, z) results in undefined because the dumpLightData function's
return statement is commented out in lightEngine.ts. To fix this, locate the
dumpLightData function implementation in lightEngine.ts, uncomment the return
statement so it properly returns the light data, and then verify that the
function returns the expected data structure to be used here.

})
}
this.logWorkerWork(() => `-> chunk ${JSON.stringify({ x, z, chunkLength: chunk.length, customBlockModelsLength: customBlockModels ? Object.keys(customBlockModels).length : 0 })}`)
Expand Down
6 changes: 4 additions & 2 deletions renderer/viewer/three/worldrendererThree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,13 @@ export class WorldRendererThree extends WorldRendererCommon {
})
this.onReactiveValueUpdated('ambientLight', (value) => {
if (!value) return
this.ambientLight.intensity = value
// this.ambientLight.intensity = value
this.ambientLight.intensity = 1
})
this.onReactiveValueUpdated('directionalLight', (value) => {
if (!value) return
this.directionalLight.intensity = value
// this.directionalLight.intensity = value
this.directionalLight.intensity = 1
})
this.onReactiveValueUpdated('lookingAtBlock', (value) => {
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
Expand Down
3 changes: 2 additions & 1 deletion src/optionsStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ const defaultOptions = {
showCursorBlockInSpectator: false,
renderEntities: true,
smoothLighting: true,
newVersionsLighting: false,
// lightingStrategy: 'prefer-server' as 'only-server' | 'always-client' | 'prefer-server',
lightingStrategy: 'prefer-server' as 'always-client' | 'prefer-server',
chatSelect: true,
autoJump: 'auto' as 'auto' | 'always' | 'never',
autoParkour: false,
Expand Down
12 changes: 8 additions & 4 deletions src/watchOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,16 @@ export const watchOptionsAfterViewerInit = () => {
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
})

subscribeKey(options, 'newVersionsLighting', () => {
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
})
const updateLightingStrategy = () => {
const clientSideLighting = options.lightingStrategy === 'always-client' || (options.lightingStrategy === 'prefer-server' && bot.supportFeature('blockStateId'))
appViewer.inWorldRenderingConfig.clientSideLighting = clientSideLighting
appViewer.inWorldRenderingConfig.enableLighting = options.dayCycleAndLighting && (!bot.supportFeature('blockStateId') || clientSideLighting)
}

subscribeKey(options, 'lightingStrategy', updateLightingStrategy)

customEvents.on('mineflayerBotCreated', () => {
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
updateLightingStrategy()
})

watchValue(options, o => {
Expand Down
Loading