RegionIO/internal/world/light.go
2026-07-21 10:04:56 +03:00

486 lines
12 KiB
Go

package world
import "regionio/internal/protocol"
// Light is stored as vanilla nibble arrays: one 2048-byte array per 16^3
// section. The two protocol-only sections below and above the world are added
// while encoding.
const lightSections = SectionCount + 2
type lightVolume struct {
minX, minZ int
width int
depth int
blocks []uint16
sky []byte
block []byte
}
type lightNode struct {
x, y, z int
}
var lightDirections = [...]lightNode{
{0, -1, 0},
{0, 1, 0},
{0, 0, -1},
{0, 0, 1},
{-1, 0, 0},
{1, 0, 0},
}
func newLightVolume(minCX, minCZ, chunksWide, chunksDeep int, chunks map[[2]int32]*Chunk) *lightVolume {
v := &lightVolume{
minX: minCX * 16,
minZ: minCZ * 16,
width: chunksWide * 16,
depth: chunksDeep * 16,
}
count := v.width * WorldHeight * v.depth
v.blocks = make([]uint16, count)
v.sky = make([]byte, count)
v.block = make([]byte, count)
for key, chunk := range chunks {
baseX := int(key[0])*16 - v.minX
baseZ := int(key[1])*16 - v.minZ
for y := MinY; y < MinY+WorldHeight; y++ {
for z := 0; z < 16; z++ {
for x := 0; x < 16; x++ {
idx := v.indexLocal(baseX+x, y, baseZ+z)
v.blocks[idx] = chunk.getBlock(x, y, z)
if chunk.lightReady {
v.sky[idx] = chunk.getLight(false, x, y, z)
v.block[idx] = chunk.getLight(true, x, y, z)
}
}
}
}
}
return v
}
func (v *lightVolume) indexLocal(x, y, z int) int {
return ((y-MinY)*v.depth+z)*v.width + x
}
func (v *lightVolume) inside(x, y, z int) bool {
return x >= 0 && x < v.width && z >= 0 && z < v.depth && y >= MinY && y < MinY+WorldHeight
}
func (v *lightVolume) calculate() {
v.calculateSky()
v.calculateBlock()
}
func (v *lightVolume) calculateSky() {
queue := make([]int, 0, len(v.sky)/2)
for z := 0; z < v.depth; z++ {
for x := 0; x < v.width; x++ {
from := StateAir
for y := MinY + WorldHeight - 1; y >= MinY; y-- {
idx := v.indexLocal(x, y, z)
state := v.blocks[idx]
if lightOpacity(state) != 0 || lightShapeOccludes(from, state, 0) {
break
}
v.sky[idx] = 15
queue = append(queue, idx)
from = state
}
}
}
v.propagateIncreases(v.sky, queue)
}
func (v *lightVolume) calculateBlock() {
queue := make([]int, 0, 256)
for idx, state := range v.blocks {
if emission := lightEmission(state); emission > 0 {
v.block[idx] = emission
queue = append(queue, idx)
}
}
v.propagateIncreases(v.block, queue)
}
func (v *lightVolume) propagateIncreases(levels []byte, queue []int) {
for head := 0; head < len(queue); head++ {
idx := queue[head]
level := levels[idx]
if level <= 1 {
continue
}
x, y, z := v.coordinates(idx)
from := v.blocks[idx]
for direction, delta := range lightDirections {
nx, ny, nz := x+delta.x, y+delta.y, z+delta.z
if !v.inside(nx, ny, nz) {
continue
}
nidx := v.indexLocal(nx, ny, nz)
into := v.blocks[nidx]
attenuation := lightOpacity(into)
if attenuation < 1 {
attenuation = 1
}
if attenuation >= level || lightShapeOccludes(from, into, direction) {
continue
}
candidate := level - attenuation
if candidate > levels[nidx] {
levels[nidx] = candidate
queue = append(queue, nidx)
}
}
}
}
func (v *lightVolume) coordinates(idx int) (x, y, z int) {
x = idx % v.width
row := idx / v.width
z = row % v.depth
y = row/v.depth + MinY
return
}
func (v *lightVolume) relaxBlockChange(x, y, z int) {
skySources := v.skySources()
blockSeeds := make([]int, 0, 7)
if v.inside(x, y, z) {
idx := v.indexLocal(x, y, z)
blockSeeds = append(blockSeeds, idx)
for _, delta := range lightDirections {
if v.inside(x+delta.x, y+delta.y, z+delta.z) {
blockSeeds = append(blockSeeds, v.indexLocal(x+delta.x, y+delta.y, z+delta.z))
}
}
}
v.relax(v.block, nil, blockSeeds)
skySeeds := make([]int, 0, WorldHeight*2)
for sy := MinY; sy < MinY+WorldHeight; sy++ {
idx := v.indexLocal(x, sy, z)
skySeeds = append(skySeeds, idx)
for _, direction := range []int{4, 5, 2, 3} {
delta := lightDirections[direction]
if v.inside(x+delta.x, sy, z+delta.z) {
skySeeds = append(skySeeds, v.indexLocal(x+delta.x, sy, z+delta.z))
}
}
}
v.relax(v.sky, skySources, skySeeds)
}
func (v *lightVolume) skySources() []bool {
sources := make([]bool, len(v.sky))
for z := 0; z < v.depth; z++ {
for x := 0; x < v.width; x++ {
from := StateAir
for y := MinY + WorldHeight - 1; y >= MinY; y-- {
idx := v.indexLocal(x, y, z)
state := v.blocks[idx]
if lightOpacity(state) != 0 || lightShapeOccludes(from, state, 0) {
break
}
sources[idx] = true
from = state
}
}
}
return sources
}
func (v *lightVolume) relax(levels []byte, sources []bool, seeds []int) {
queued := make([]bool, len(levels))
queue := make([]int, 0, len(seeds)*2)
for _, idx := range seeds {
if idx >= 0 && idx < len(levels) && !queued[idx] {
queued[idx] = true
queue = append(queue, idx)
}
}
for head := 0; head < len(queue); head++ {
idx := queue[head]
queued[idx] = false
desired := v.desiredLight(levels, sources, idx)
if desired == levels[idx] {
continue
}
levels[idx] = desired
x, y, z := v.coordinates(idx)
for _, delta := range lightDirections {
nx, ny, nz := x+delta.x, y+delta.y, z+delta.z
if !v.inside(nx, ny, nz) {
continue
}
nidx := v.indexLocal(nx, ny, nz)
if !queued[nidx] {
queued[nidx] = true
queue = append(queue, nidx)
}
}
}
}
func (v *lightVolume) desiredLight(levels []byte, sources []bool, idx int) byte {
state := v.blocks[idx]
desired := lightEmission(state)
if sources != nil {
desired = 0
if sources[idx] {
desired = 15
}
}
attenuation := lightOpacity(state)
if attenuation < 1 {
attenuation = 1
}
x, y, z := v.coordinates(idx)
opposite := [...]int{1, 0, 3, 2, 5, 4}
for direction, delta := range lightDirections {
nx, ny, nz := x+delta.x, y+delta.y, z+delta.z
if !v.inside(nx, ny, nz) {
continue
}
nidx := v.indexLocal(nx, ny, nz)
neighbor := levels[nidx]
if neighbor <= attenuation || lightShapeOccludes(v.blocks[nidx], state, opposite[direction]) {
continue
}
candidate := neighbor - attenuation
if candidate > desired {
desired = candidate
}
}
return desired
}
func (c *Chunk) getLight(block bool, x, y, z int) byte {
si := (y - MinY) >> 4
if si < 0 || si >= SectionCount {
return 0
}
layers := &c.skyLight
if block {
layers = &c.blockLight
}
section := layers[si]
if section == nil {
return 0
}
idx := blockIndex(x, y, z)
b := section[idx>>1]
if idx&1 == 0 {
return b & 0x0f
}
return b >> 4
}
func (c *Chunk) setLight(block bool, x, y, z int, value byte) bool {
si := (y - MinY) >> 4
if si < 0 || si >= SectionCount {
return false
}
layers := &c.skyLight
if block {
layers = &c.blockLight
}
section := layers[si]
if section == nil {
if value == 0 {
return false
}
section = new([2048]byte)
layers[si] = section
}
idx := blockIndex(x, y, z)
old := section[idx>>1]
if idx&1 == 0 {
section[idx>>1] = old&0xf0 | value&0x0f
} else {
section[idx>>1] = old&0x0f | value<<4
}
return old != section[idx>>1]
}
// LightAt returns the stored sky and block light at a local block coordinate.
func (c *Chunk) LightAt(x, y, z int) (sky, block byte, ready bool) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.getLight(false, x, y, z), c.getLight(true, x, y, z), c.lightReady
}
func (c *Chunk) installLight(v *lightVolume) bool {
changed := !c.lightReady
var sky [SectionCount]*[2048]byte
var block [SectionCount]*[2048]byte
baseX := int(c.X)*16 - v.minX
baseZ := int(c.Z)*16 - v.minZ
for y := MinY; y < MinY+WorldHeight; y++ {
for z := 0; z < 16; z++ {
for x := 0; x < 16; x++ {
idx := v.indexLocal(baseX+x, y, baseZ+z)
installNibble(&sky, x, y, z, v.sky[idx])
installNibble(&block, x, y, z, v.block[idx])
}
}
}
if !lightLayersEqual(c.skyLight, sky) || !lightLayersEqual(c.blockLight, block) {
changed = true
}
c.skyLight = sky
c.blockLight = block
c.lightReady = true
c.lightValidated = true
return changed
}
func installNibble(layers *[SectionCount]*[2048]byte, x, y, z int, value byte) {
if value == 0 {
return
}
si := (y - MinY) >> 4
if layers[si] == nil {
layers[si] = new([2048]byte)
}
idx := blockIndex(x, y, z)
if idx&1 == 0 {
layers[si][idx>>1] |= value
} else {
layers[si][idx>>1] |= value << 4
}
}
func lightLayersEqual(a, b [SectionCount]*[2048]byte) bool {
for i := 0; i < SectionCount; i++ {
if a[i] == nil || b[i] == nil {
if a[i] != nil || b[i] != nil {
return false
}
continue
}
if *a[i] != *b[i] {
return false
}
}
return true
}
func (c *Chunk) writeLight(w *protocol.Writer) {
sky, block, highest := c.skyLight, c.blockLight, c.highestFilledSection()
if !c.lightReady {
chunks := map[[2]int32]*Chunk{{c.X, c.Z}: c}
v := newLightVolume(int(c.X), int(c.Z), 1, 1, chunks)
v.calculate()
standalone := &Chunk{X: c.X, Z: c.Z}
standalone.installLight(v)
sky, block = standalone.skyLight, standalone.blockLight
}
maxLightSection := highest + 2
if maxLightSection < 0 {
maxLightSection = 0
}
writeLightLayers(w, sky, block, maxLightSection)
}
func (c *Chunk) highestFilledSection() int {
for si := SectionCount - 1; si >= 0; si-- {
if c.sections[si] == nil {
continue
}
for _, state := range c.sections[si] {
if state != StateAir {
return si
}
}
}
return -1
}
func writeLightLayers(w *protocol.Writer, sky, block [SectionCount]*[2048]byte, maxLightSection int) {
if maxLightSection >= lightSections {
maxLightSection = lightSections - 1
}
var skySections [lightSections]*[2048]byte
var blockSections [lightSections]*[2048]byte
for i := 0; i < SectionCount && i+1 <= maxLightSection; i++ {
skySections[i+1] = sky[i]
blockSections[i+1] = block[i]
}
if maxLightSection == lightSections-1 {
skySections[maxLightSection] = new([2048]byte)
for i := range skySections[maxLightSection] {
skySections[maxLightSection][i] = 0xff
}
}
var skyMask, blockMask, emptySkyMask, emptyBlockMask uint64
for i := 0; i <= maxLightSection; i++ {
if skySections[i] == nil {
emptySkyMask |= 1 << i
} else {
skyMask |= 1 << i
}
if blockSections[i] == nil {
emptyBlockMask |= 1 << i
} else {
blockMask |= 1 << i
}
}
writeBitSet(w, []uint64{skyMask})
writeBitSet(w, []uint64{blockMask})
writeBitSet(w, []uint64{emptySkyMask})
writeBitSet(w, []uint64{emptyBlockMask})
writeLightArrays(w, skySections[:])
writeLightArrays(w, blockSections[:])
}
func writeLightArrays(w *protocol.Writer, sections []*[2048]byte) {
count := 0
for _, section := range sections {
if section != nil {
count++
}
}
w.VarInt(int32(count))
for _, section := range sections {
if section != nil {
w.VarInt(2048)
w.Raw(section[:])
}
}
}
// EncodeLightUpdate serializes a standalone light_update packet body from a
// consistent chunk snapshot.
func (c *Chunk) EncodeLightUpdate() []byte {
snapshot, _ := c.snapshot()
w := protocol.NewWriter(8192)
w.VarInt(snapshot.X)
w.VarInt(snapshot.Z)
sky, block := snapshot.skyLight, snapshot.blockLight
if !snapshot.lightReady {
chunks := map[[2]int32]*Chunk{{snapshot.X, snapshot.Z}: snapshot}
volume := newLightVolume(int(snapshot.X), int(snapshot.Z), 1, 1, chunks)
volume.calculate()
standalone := &Chunk{X: snapshot.X, Z: snapshot.Z}
standalone.installLight(volume)
sky, block = standalone.skyLight, standalone.blockLight
}
writeLightLayers(w, sky, block, lightSections-1)
return w.Bytes()
}
func allSectionsMask() []uint64 {
return []uint64{(uint64(1) << lightSections) - 1}
}
func writeBitSet(w *protocol.Writer, longs []uint64) {
for len(longs) > 0 && longs[len(longs)-1] == 0 {
longs = longs[:len(longs)-1]
}
w.VarInt(int32(len(longs)))
for _, value := range longs {
w.Int64(int64(value))
}
}