26ed99fda6
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
289 lines
8.7 KiB
Go
289 lines
8.7 KiB
Go
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)
|
|
}
|