package chunk import ( "bytes" "math" "github.com/df-mc/dragonfly/server/block/cube" ) // lightArea represents a square area of N*N chunks. It is used for light calculation specifically. type lightArea struct { br BlockRegistry baseX, baseZ int c []*Chunk w int r cube.Range } // lightQueue is a FIFO ring buffer used during light propagation. type lightQueue struct { nodes []lightNode head int tail int size int } // initialLightQueueCapacity is the starting size for light propagation queues. A lightNode is 48 bytes on // 64-bit platforms, so 1024 entries cost about 48 KiB. This avoids the first grow/copy for busier lighting // runs while keeping the queue transient and able to grow for larger chunks. const initialLightQueueCapacity = 1024 // newLightQueue creates an empty queue sized to capacity (at least 1). func newLightQueue(capacity int) *lightQueue { if capacity < 1 { capacity = 1 } return &lightQueue{nodes: make([]lightNode, capacity)} } // push appends a node to the tail, growing storage if full. func (q *lightQueue) push(n lightNode) { if q.size == len(q.nodes) { q.grow() } q.nodes[q.tail] = n q.tail = (q.tail + 1) % len(q.nodes) q.size++ } // pop removes and returns the oldest queued node. func (q *lightQueue) pop() (lightNode, bool) { if q.size == 0 { return lightNode{}, false } n := q.nodes[q.head] q.head = (q.head + 1) % len(q.nodes) q.size-- return n, true } // empty returns true when no nodes are queued. func (q *lightQueue) empty() bool { return q.size == 0 } // grow expands the ring buffer and reorders elements to start at index 0. func (q *lightQueue) grow() { nodes := make([]lightNode, len(q.nodes)<<1) if q.head < q.tail { copy(nodes, q.nodes[q.head:q.tail]) } else { n := copy(nodes, q.nodes[q.head:]) copy(nodes[n:], q.nodes[:q.tail]) } q.head = 0 q.tail = q.size q.nodes = nodes } // LightArea creates a lightArea with the lower corner of the lightArea at baseX and baseZ. The length of the Chunk // slice must be a square of a number, so 1, 4, 9 etc. func LightArea(c []*Chunk, baseX, baseZ int) *lightArea { w := int(math.Sqrt(float64(len(c)))) if len(c) != w*w { panic("area must have a square chunk area") } return &lightArea{ br: c[0].br, c: c, w: w, baseX: baseX << 4, baseZ: baseZ << 4, r: c[0].r, } } // Fill executes the light 'filling' stage, where the lightArea is filled with light coming only from the // individual chunks within the lightArea itself, without light crossing chunk borders. func (a *lightArea) Fill() { a.initialiseLightSlices() queue := newLightQueue(initialLightQueueCapacity) a.insertBlockLightNodes(queue) a.insertSkyLightNodes(queue) for !queue.empty() { a.propagate(queue) } } // Spread executes the light 'spreading' stage, where the lightArea has light spread from every Chunk into the // neighbouring chunks. The neighbouring chunks must have passed the light 'filling' stage before this // function is called for an lightArea that includes them. func (a *lightArea) Spread() { queue := newLightQueue(initialLightQueueCapacity) a.insertLightSpreadingNodes(queue, BlockLight) a.insertLightSpreadingNodes(queue, SkyLight) for !queue.empty() { a.propagate(queue) } } // light returns the light at a cube.Pos with the light type l. func (a *lightArea) light(pos cube.Pos, l light) uint8 { return l.light(a.sub(pos), uint8(pos[0]&0xf), uint8(pos[1]&0xf), uint8(pos[2]&0xf)) } // light sets the light at a cube.Pos with the light type l. func (a *lightArea) setLight(pos cube.Pos, l light, v uint8) { l.setLight(a.sub(pos), uint8(pos[0]&0xf), uint8(pos[1]&0xf), uint8(pos[2]&0xf), v) } // iterSubChunks iterates over all blocks of the lightArea on a per-SubChunk basis. A filter function may be passed to // specify if a SubChunk should be iterated over. If it returns false, it will not be iterated over. func (a *lightArea) iterSubChunks(filter func(sub *SubChunk) bool, f func(pos cube.Pos)) { for cx := 0; cx < a.w; cx++ { for cz := 0; cz < a.w; cz++ { baseX, baseZ, c := a.baseX+(cx<<4), a.baseZ+(cz<<4), a.c[a.chunkIndex(cx, cz)] for index, sub := range c.sub { if !filter(sub) { continue } baseY := int(c.SubY(int16(index))) a.iterSubChunk(func(x, y, z int) { f(cube.Pos{x + baseX, y + baseY, z + baseZ}) }) } } } } // iterEdges iterates over all chunk edges within the lightArea and calls the function f with the cube.Pos at either // side of the edge. func (a *lightArea) iterEdges(filter func(a, b *SubChunk) bool, f func(a, b cube.Pos)) { minY, maxY := a.r[0]>>4, a.r[1]>>4 // First iterate over chunk X, Y and Z, so we can filter out a complete 16x16 sheet of blocks if the // filter function returns false. for cu := 1; cu < a.w; cu++ { u := cu << 4 for cv := 0; cv < a.w; cv++ { v := cv << 4 for cy := minY; cy < maxY; cy++ { baseY := cy << 4 xa, za := cube.Pos{a.baseX + u, baseY, a.baseZ + v}, cube.Pos{a.baseX + v, baseY, a.baseZ + u} xb, zb := xa.Side(cube.FaceWest), za.Side(cube.FaceNorth) addX, addZ := filter(a.sub(xa), a.sub(xb)), filter(a.sub(za), a.sub(zb)) if !addX && !addZ { continue } // The order of these loops allows us to take care of block spreading over both the X and Z axis by // just swapping around the axes. for addV := 0; addV < 16; addV++ { for y := 0; y < 16; y++ { // Finally, iterate over the 16x16 sheet and actually do the per-block checks. if addX { f(xa.Add(cube.Pos{0, y, addV}), xb.Add(cube.Pos{0, y, addV})) } if addZ { f(za.Add(cube.Pos{addV, y}), zb.Add(cube.Pos{addV, y})) } } } } } } } // iterHeightmap iterates over the height map of the lightArea and calls the function f with the height map value, the // height map value of the highest neighbour and the Y value of the highest non-empty SubChunk. func (a *lightArea) iterHeightmap(f func(x, z int, height, highestNeighbour, highestY, lowestY int)) { m, highestY := a.c[0].HeightMap(), a.c[0].Range().Min() lowestY := highestY for index := range a.c[0].sub { if a.c[0].sub[index].Empty() { continue } highestY = int(a.c[0].SubY(int16(index))) + 15 } for x := uint8(0); x < 16; x++ { for z := uint8(0); z < 16; z++ { f(int(x)+a.baseX, int(z)+a.baseZ, int(m.At(x, z)), int(m.HighestNeighbour(x, z)), highestY, lowestY) } } } // iterSubChunk iterates over the coordinates of a SubChunk (0-15 on all axes) and calls the function f for each of // those coordinates. func (a *lightArea) iterSubChunk(f func(x, y, z int)) { for y := 0; y < 16; y++ { for x := 0; x < 16; x++ { for z := 0; z < 16; z++ { f(x, y, z) } } } } // highest looks up through the blocks at first and second layer at the cube.Pos passed, calls the lightBlocking // function for each runtime ID, and returns the highest value. func (a *lightArea) highest(pos cube.Pos, lightBlocking func(rid uint32) uint8) uint8 { x, y, z, sub := uint8(pos[0]&0xf), uint8(pos[1]&0xf), uint8(pos[2]&0xf), a.sub(pos) storages, l := sub.storages, len(sub.storages) switch l { case 0: return 0 case 1: return lightBlocking(storages[0].At(x, y, z)) default: level := lightBlocking(storages[0].At(x, y, z)) if v := lightBlocking(storages[1].At(x, y, z)); v > level { return v } return level } } var ( fullLight = bytes.Repeat([]byte{0xff}, 2048) fullLightPtr = &fullLight[0] noLight = make([]uint8, 2048) noLightPtr = &noLight[0] ) // initialiseLightSlices initialises all light slices in the sub chunks of all chunks either with full light if there is // no sub chunk with any blocks above it, or with empty light if there is. The sub chunks with empty light are then // ready to be properly calculated. func (a *lightArea) initialiseLightSlices() { for _, c := range a.c { index := len(c.sub) - 1 for index >= 0 { sub := c.sub[index] if !sub.Empty() { // We've hit the topmost empty SubChunk. break } sub.skyLight = fullLight sub.blockLight = noLight index-- } for index >= 0 { // Fill up the rest of the sub chunks with empty light. We will do light calculation for these sub chunks // later on. c.sub[index].skyLight = noLight c.sub[index].blockLight = noLight index-- } } } // sub returns the SubChunk corresponding to a cube.Pos. func (a *lightArea) sub(pos cube.Pos) *SubChunk { return a.chunk(pos).SubChunk(int16(pos[1])) } // chunk returns the Chunk corresponding to a cube.Pos. func (a *lightArea) chunk(pos cube.Pos) *Chunk { x, z := pos[0]-a.baseX, pos[2]-a.baseZ return a.c[a.chunkIndex(x>>4, z>>4)] } // chunkIndex finds the index in the chunk slice of an lightArea for a Chunk at a specific x and z. func (a *lightArea) chunkIndex(x, z int) int { return x + (z * a.w) }