up3
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
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
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package chunk
|
||||
|
||||
// BlockRegistry provides the minimal block registry API required by the chunk package.
|
||||
//
|
||||
// Implementations must be safe for concurrent read access after construction/finalization, as chunks and chunk
|
||||
// encoding/decoding may happen from multiple goroutines.
|
||||
type BlockRegistry interface {
|
||||
// BlockCount returns the number of runtime IDs known by the registry.
|
||||
BlockCount() int
|
||||
// AirRuntimeID returns the runtime ID representing air.
|
||||
AirRuntimeID() uint32
|
||||
// RuntimeIDToState resolves a runtime ID to its (name, properties) state representation.
|
||||
RuntimeIDToState(runtimeID uint32) (name string, properties map[string]any, found bool)
|
||||
// StateToRuntimeID resolves a (name, properties) state representation to a runtime ID.
|
||||
StateToRuntimeID(name string, properties map[string]any) (runtimeID uint32, found bool)
|
||||
// FilteringBlock returns the light filtering value for the runtime ID.
|
||||
FilteringBlock(rid uint32) uint8
|
||||
// LightBlock returns the light emission value for the runtime ID.
|
||||
LightBlock(rid uint32) uint8
|
||||
// RandomTickBlock reports whether the runtime ID receives random ticks.
|
||||
RandomTickBlock(rid uint32) bool
|
||||
// NBTBlock reports whether the runtime ID uses NBT data and requires NBT-aware encoding/decoding.
|
||||
NBTBlock(rid uint32) bool
|
||||
// LiquidDisplacingBlock reports whether the runtime ID displaces liquids.
|
||||
LiquidDisplacingBlock(rid uint32) bool
|
||||
// LiquidBlock reports whether the runtime ID represents a liquid block.
|
||||
LiquidBlock(rid uint32) bool
|
||||
// HashToRuntimeID resolves a "network block hash" to a runtime ID.
|
||||
HashToRuntimeID(hash uint32) (rid uint32, ok bool)
|
||||
// RuntimeIDToHash resolves a runtime ID to its "network block hash".
|
||||
RuntimeIDToHash(runtimeID uint32) (hash uint32, ok bool)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
)
|
||||
|
||||
// Chunk is a segment in the world with a size of 16x16x256 blocks. A chunk contains multiple sub chunks
|
||||
// and stores other information such as biomes.
|
||||
// It is not safe to call methods on Chunk simultaneously from multiple goroutines.
|
||||
type Chunk struct {
|
||||
// r holds the (vertical) range of the Chunk. It includes both the minimum and maximum coordinates.
|
||||
r cube.Range
|
||||
// br is the block registry used for this chunk.
|
||||
br BlockRegistry
|
||||
// air is the runtime ID of air.
|
||||
air uint32
|
||||
// recalculateHeightMap is true if the chunk's height map should be recalculated on the next call to the HeightMap
|
||||
// function.
|
||||
recalculateHeightMap bool
|
||||
// heightMap is the height map of the chunk.
|
||||
heightMap HeightMap
|
||||
// sub holds all sub chunks part of the chunk. The pointers held by the array are nil if no sub chunk is
|
||||
// allocated at the indices.
|
||||
sub []*SubChunk
|
||||
// biomes is an array of biome IDs. There is one biome ID for every column in the chunk.
|
||||
biomes []*PalettedStorage
|
||||
}
|
||||
|
||||
// New initialises a new chunk and returns it, so that it may be used.
|
||||
// The BlockRegistry passed must be finalized and must correspond to the runtime IDs used in the chunk's storages.
|
||||
func New(br BlockRegistry, r cube.Range) *Chunk {
|
||||
n := (r.Height() >> 4) + 1
|
||||
sub, biomes := make([]*SubChunk, n), make([]*PalettedStorage, n)
|
||||
air := br.AirRuntimeID()
|
||||
for i := 0; i < n; i++ {
|
||||
sub[i] = NewSubChunk(air)
|
||||
biomes[i] = emptyStorage(0)
|
||||
}
|
||||
return &Chunk{
|
||||
r: r,
|
||||
br: br,
|
||||
air: air,
|
||||
sub: sub,
|
||||
biomes: biomes,
|
||||
recalculateHeightMap: true,
|
||||
heightMap: make(HeightMap, 256),
|
||||
}
|
||||
}
|
||||
|
||||
// Clone returns an independent copy of the Chunk.
|
||||
func (chunk *Chunk) Clone() *Chunk {
|
||||
clone := &Chunk{
|
||||
r: chunk.r,
|
||||
br: chunk.br,
|
||||
air: chunk.air,
|
||||
recalculateHeightMap: chunk.recalculateHeightMap,
|
||||
heightMap: slices.Clone(chunk.heightMap),
|
||||
sub: make([]*SubChunk, len(chunk.sub)),
|
||||
biomes: make([]*PalettedStorage, len(chunk.biomes)),
|
||||
}
|
||||
for i, sub := range chunk.sub {
|
||||
clone.sub[i] = sub.Clone()
|
||||
}
|
||||
for i, biomes := range chunk.biomes {
|
||||
clone.biomes[i] = biomes.Clone()
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// Equals returns if the chunk passed is equal to the current one
|
||||
func (chunk *Chunk) Equals(c *Chunk) bool {
|
||||
if !chunk.recalculateHeightMap && !c.recalculateHeightMap && !slices.Equal(c.heightMap, chunk.heightMap) {
|
||||
return false
|
||||
}
|
||||
|
||||
if c.r != chunk.r || c.air != chunk.air || len(c.sub) != len(chunk.sub) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, s := range c.sub {
|
||||
if !s.Equals(chunk.sub[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Range returns the cube.Range of the Chunk as passed to New.
|
||||
func (chunk *Chunk) Range() cube.Range {
|
||||
return chunk.r
|
||||
}
|
||||
|
||||
// Sub returns a list of all sub chunks present in the chunk.
|
||||
func (chunk *Chunk) Sub() []*SubChunk {
|
||||
return chunk.sub
|
||||
}
|
||||
|
||||
// Block returns the runtime ID of the block at a given x, y and z in a chunk at the given layer. If no
|
||||
// sub chunk exists at the given y, the block is assumed to be air.
|
||||
func (chunk *Chunk) Block(x uint8, y int16, z uint8, layer uint8) uint32 {
|
||||
sub := chunk.SubChunk(y)
|
||||
if sub.Empty() || uint8(len(sub.storages)) <= layer {
|
||||
return chunk.air
|
||||
}
|
||||
return sub.storages[layer].At(x, uint8(y), z)
|
||||
}
|
||||
|
||||
// SetBlock sets the runtime ID of a block at a given x, y and z in a chunk at the given layer. If no
|
||||
// SubChunk exists at the given y, a new SubChunk is created and the block is set.
|
||||
func (chunk *Chunk) SetBlock(x uint8, y int16, z uint8, layer uint8, block uint32) {
|
||||
sub := chunk.sub[chunk.SubIndex(y)]
|
||||
if uint8(len(sub.storages)) <= layer && block == chunk.air {
|
||||
// Air was set at n layer, but there were less than n layers, so there already was air there.
|
||||
// Don't do anything with this, just return.
|
||||
return
|
||||
}
|
||||
sub.Layer(layer).Set(x, uint8(y), z, block)
|
||||
chunk.recalculateHeightMap = true
|
||||
}
|
||||
|
||||
// Biome returns the biome ID at a specific column in the chunk.
|
||||
func (chunk *Chunk) Biome(x uint8, y int16, z uint8) uint32 {
|
||||
return chunk.biomes[chunk.SubIndex(y)].At(x, uint8(y), z)
|
||||
}
|
||||
|
||||
// SetBiome sets the biome ID at a specific column in the chunk.
|
||||
func (chunk *Chunk) SetBiome(x uint8, y int16, z uint8, biome uint32) {
|
||||
chunk.biomes[chunk.SubIndex(y)].Set(x, uint8(y), z, biome)
|
||||
}
|
||||
|
||||
// Light returns the light level at a specific position in the chunk.
|
||||
func (chunk *Chunk) Light(x uint8, y int16, z uint8) uint8 {
|
||||
ux, uy, uz, sub := x&0xf, uint8(y&0xf), z&0xf, chunk.SubChunk(y)
|
||||
sky := sub.SkyLight(ux, uy, uz)
|
||||
if sky == 15 {
|
||||
// The skylight was already on the maximum value, so return it without checking block light.
|
||||
return sky
|
||||
}
|
||||
if block := sub.BlockLight(ux, uy, uz); block > sky {
|
||||
return block
|
||||
}
|
||||
return sky
|
||||
}
|
||||
|
||||
// SkyLight returns the skylight level at a specific position in the chunk.
|
||||
func (chunk *Chunk) SkyLight(x uint8, y int16, z uint8) uint8 {
|
||||
return chunk.SubChunk(y).SkyLight(x&15, uint8(y&15), z&15)
|
||||
}
|
||||
|
||||
// HighestLightBlocker iterates from the highest non-empty sub chunk downwards to find the Y value of the
|
||||
// highest block that completely blocks any light from going through. If none is found, the value returned is
|
||||
// the minimum height.
|
||||
func (chunk *Chunk) HighestLightBlocker(x, z uint8) int16 {
|
||||
return chunk.highestLightBlocker(x, z, false)
|
||||
}
|
||||
|
||||
// highestLightBlocker iterates from the highest non-empty sub chunk downwards
|
||||
// to find the Y value of the highest block that completely blocks any light
|
||||
// from going through. If none is found, the value returned is the minimum
|
||||
// height. If addOne is true, one is added to the Y returned if a block was
|
||||
// found.
|
||||
func (chunk *Chunk) highestLightBlocker(x, z uint8, addOne bool) int16 {
|
||||
var plus int16
|
||||
if addOne {
|
||||
plus++
|
||||
}
|
||||
for index := int16(len(chunk.sub) - 1); index >= 0; index-- {
|
||||
if sub := chunk.sub[index]; !sub.Empty() {
|
||||
for y := 15; y >= 0; y-- {
|
||||
if chunk.br.FilteringBlock(sub.storages[0].At(x, uint8(y), z)) == 15 {
|
||||
return int16(y) | chunk.SubY(index) + plus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return int16(chunk.r[0])
|
||||
}
|
||||
|
||||
// HighestBlock iterates from the highest non-empty sub chunk downwards to find the Y value of the highest
|
||||
// non-air block at an x and z. If no blocks are present in the column, the minimum height is returned.
|
||||
func (chunk *Chunk) HighestBlock(x, z uint8) int16 {
|
||||
for index := int16(len(chunk.sub) - 1); index >= 0; index-- {
|
||||
if sub := chunk.sub[index]; !sub.Empty() {
|
||||
for y := 15; y >= 0; y-- {
|
||||
if rid := sub.storages[0].At(x, uint8(y), z); rid != chunk.air {
|
||||
return int16(y) | chunk.SubY(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return int16(chunk.r[0])
|
||||
}
|
||||
|
||||
// HeightMap returns the height map of the chunk. If the chunk is edited, the height map will be recalculated on the
|
||||
// next call to this function.
|
||||
func (chunk *Chunk) HeightMap() HeightMap {
|
||||
if chunk.recalculateHeightMap {
|
||||
for x := uint8(0); x < 16; x++ {
|
||||
for z := uint8(0); z < 16; z++ {
|
||||
chunk.heightMap.Set(x, z, chunk.highestLightBlocker(x, z, true))
|
||||
}
|
||||
}
|
||||
chunk.recalculateHeightMap = false
|
||||
}
|
||||
return chunk.heightMap
|
||||
}
|
||||
|
||||
// Compact compacts the chunk as much as possible, getting rid of any sub chunks that are empty, and compacts
|
||||
// all storages in the sub chunks to occupy as little space as possible.
|
||||
// Compact should be called right before the chunk is saved in order to optimise the storage space.
|
||||
func (chunk *Chunk) Compact() {
|
||||
for i := range chunk.sub {
|
||||
chunk.sub[i].compact()
|
||||
}
|
||||
}
|
||||
|
||||
// SubChunk finds the correct SubChunk in the Chunk by a Y value.
|
||||
func (chunk *Chunk) SubChunk(y int16) *SubChunk {
|
||||
return chunk.sub[chunk.SubIndex(y)]
|
||||
}
|
||||
|
||||
// SubIndex returns the sub chunk Y index matching the y value passed.
|
||||
func (chunk *Chunk) SubIndex(y int16) int16 {
|
||||
return (y - int16(chunk.r[0])) >> 4
|
||||
}
|
||||
|
||||
// SubY returns the sub chunk Y value matching the index passed.
|
||||
func (chunk *Chunk) SubY(index int16) int16 {
|
||||
return (index << 4) + int16(chunk.r[0])
|
||||
}
|
||||
|
||||
// HighestFilledSubChunk returns the number of sub chunks up to and including the
|
||||
// highest sub chunk in the chunk that has any blocks in it. 0 is returned if no
|
||||
// subchunks have any blocks.
|
||||
func (chunk *Chunk) HighestFilledSubChunk() uint16 {
|
||||
for i, sub := range slices.Backward(chunk.sub) {
|
||||
if !sub.Empty() {
|
||||
return uint16(i + 1)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
)
|
||||
|
||||
type Column struct {
|
||||
Chunk *Chunk
|
||||
Entities []Entity
|
||||
BlockEntities []BlockEntity
|
||||
Tick int64
|
||||
ScheduledBlocks []ScheduledBlockUpdate
|
||||
}
|
||||
|
||||
type BlockEntity struct {
|
||||
Pos cube.Pos
|
||||
Data map[string]any
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
ID int64
|
||||
Data map[string]any
|
||||
}
|
||||
|
||||
type ScheduledBlockUpdate struct {
|
||||
Pos cube.Pos
|
||||
Block uint32
|
||||
Tick int64
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"github.com/sandertv/gophertunnel/minecraft/nbt"
|
||||
)
|
||||
|
||||
// legacyBlockEntry represents a block entry used in versions prior to 1.13.
|
||||
type legacyBlockEntry struct {
|
||||
Name string `nbt:"name"`
|
||||
Meta int16 `nbt:"meta"`
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed legacy_states.nbt
|
||||
legacyMappingsData []byte
|
||||
// legacyMappings allows simple conversion from a legacy block entry to a new one.
|
||||
legacyMappings = make(map[legacyBlockEntry]blockEntry)
|
||||
)
|
||||
|
||||
// upgradeLegacyEntry upgrades a legacy block entry to a new one.
|
||||
func upgradeLegacyEntry(name string, meta int16) (blockEntry, bool) {
|
||||
entry, ok := legacyMappings[legacyBlockEntry{Name: name, Meta: meta}]
|
||||
if !ok {
|
||||
// Also try cases where the meta should be disregarded.
|
||||
entry, ok = legacyMappings[legacyBlockEntry{Name: name}]
|
||||
}
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
// init creates conversions for each legacy and alias entry.
|
||||
func init() {
|
||||
var entry struct {
|
||||
Legacy legacyBlockEntry `nbt:"legacy"`
|
||||
Updated blockEntry `nbt:"updated"`
|
||||
}
|
||||
dec := nbt.NewDecoder(bytes.NewBuffer(legacyMappingsData))
|
||||
for {
|
||||
if err := dec.Decode(&entry); err != nil {
|
||||
break
|
||||
}
|
||||
legacyMappings[entry.Legacy] = entry.Updated
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
)
|
||||
|
||||
// NetworkDecode decodes the network serialised data passed into a Chunk if successful. If not, the chunk
|
||||
// returned is nil and the error non-nil.
|
||||
// The sub chunk count passed must be that found in the LevelChunk packet.
|
||||
// NetworkDecode creates a new buffer and calls NetworkDecodeBuffer.
|
||||
//
|
||||
// The BlockRegistry passed must be finalized and must correspond to the runtime IDs used in the chunk data.
|
||||
// noinspection GoUnusedExportedFunction
|
||||
func NetworkDecode(br BlockRegistry, data []byte, count int, r cube.Range) (*Chunk, error) {
|
||||
return NetworkDecodeBuffer(br, bytes.NewBuffer(data), count, r)
|
||||
}
|
||||
|
||||
// NetworkDecodeBuffer decodes the network serialised data from buf passed into a Chunk if successful. If not, the chunk
|
||||
// returned is nil and the error non-nil.
|
||||
// The sub chunk count passed must be that found in the LevelChunk packet.
|
||||
// noinspection GoUnusedExportedFunction
|
||||
func NetworkDecodeBuffer(br BlockRegistry, buf *bytes.Buffer, count int, r cube.Range) (*Chunk, error) {
|
||||
var (
|
||||
c = New(br, r)
|
||||
err error
|
||||
)
|
||||
for i := 0; i < count; i++ {
|
||||
index := uint8(i)
|
||||
c.sub[index], err = decodeSubChunk(buf, c, &index, NetworkEncoding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var last *PalettedStorage
|
||||
for i := 0; i < len(c.sub); i++ {
|
||||
b, err := decodePalettedStorage(buf, NetworkEncoding, BiomePaletteEncoding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if b == nil {
|
||||
// b == nil means this paletted storage had the flag pointing to the previous one. It basically means we should
|
||||
// inherit whatever palette we decoded last.
|
||||
if i == 0 {
|
||||
// This should never happen and there is no way to handle this.
|
||||
return nil, fmt.Errorf("first biome storage pointed to previous one")
|
||||
}
|
||||
b = last
|
||||
} else {
|
||||
last = b
|
||||
}
|
||||
c.biomes[i] = b
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DiskDecode decodes the data from a SerialisedData object into a chunk and returns it. If the data was invalid,
|
||||
// an error is returned.
|
||||
//
|
||||
// The BlockRegistry passed must be finalized and must correspond to the runtime IDs used in the chunk data.
|
||||
func DiskDecode(br BlockRegistry, data SerialisedData, r cube.Range) (*Chunk, error) {
|
||||
c := New(br, r)
|
||||
|
||||
err := decodeBiomes(bytes.NewBuffer(data.Biomes), c, DiskEncoding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, sub := range data.SubChunks {
|
||||
if len(sub) == 0 {
|
||||
// No data for this sub chunk.
|
||||
continue
|
||||
}
|
||||
index := uint8(i)
|
||||
if c.sub[index], err = decodeSubChunk(bytes.NewBuffer(sub), c, &index, DiskEncoding); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// decodeSubChunk decodes a SubChunk from a bytes.Buffer. The Encoding passed defines how the block storages of the
|
||||
// SubChunk are decoded.
|
||||
func decodeSubChunk(buf *bytes.Buffer, c *Chunk, index *byte, e Encoding) (*SubChunk, error) {
|
||||
ver, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading version: %w", err)
|
||||
}
|
||||
sub := NewSubChunk(c.air)
|
||||
switch ver {
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown sub chunk version %v: can't decode", ver)
|
||||
case 1:
|
||||
// Version 1 only has one layer for each sub chunk, but uses the format with palettes.
|
||||
storage, err := decodePalettedStorage(buf, e, BlockPaletteEncoding{Blocks: c.br})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub.storages = append(sub.storages, storage)
|
||||
case 8, 9:
|
||||
// Version 8 allows up to 256 layers for one sub chunk.
|
||||
storageCount, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading storage count: %w", err)
|
||||
}
|
||||
if ver == 9 {
|
||||
uIndex, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading sub-chunk index: %w", err)
|
||||
}
|
||||
// The index as written here isn't the actual index of the sub-chunk within the chunk. Rather, it is the Y
|
||||
// value of the sub-chunk. This means that we need to translate it to an index.
|
||||
*index = uint8(int8(uIndex) - int8(c.r[0]>>4))
|
||||
}
|
||||
sub.storages = make([]*PalettedStorage, storageCount)
|
||||
|
||||
for i := byte(0); i < storageCount; i++ {
|
||||
sub.storages[i], err = decodePalettedStorage(buf, e, BlockPaletteEncoding{Blocks: c.br})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
// decodeBiomes reads the paletted storages holding biomes from buf and stores it into the Chunk passed.
|
||||
func decodeBiomes(buf *bytes.Buffer, c *Chunk, e Encoding) error {
|
||||
var last *PalettedStorage
|
||||
if buf.Len() != 0 {
|
||||
for i := 0; i < len(c.sub); i++ {
|
||||
b, err := decodePalettedStorage(buf, e, BiomePaletteEncoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// b == nil means this paletted storage had the flag pointing to the previous one. It basically means we should
|
||||
// inherit whatever palette we decoded last.
|
||||
if i == 0 && b == nil {
|
||||
// This should never happen and there is no way to handle this.
|
||||
return fmt.Errorf("first biome storage pointed to previous one")
|
||||
}
|
||||
if b == nil {
|
||||
// This means this paletted storage had the flag pointing to the previous one. It basically means we should
|
||||
// inherit whatever palette we decoded last.
|
||||
b = last
|
||||
} else {
|
||||
last = b
|
||||
}
|
||||
c.biomes[i] = b
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodePalettedStorage decodes a PalettedStorage from a bytes.Buffer. The Encoding passed is used to read either a
|
||||
// network or disk block storage.
|
||||
func decodePalettedStorage(buf *bytes.Buffer, e Encoding, pe paletteEncoding) (*PalettedStorage, error) {
|
||||
blockSize, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading block size: %w", err)
|
||||
}
|
||||
blockSize >>= 1
|
||||
if blockSize == 0x7f {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
size := paletteSize(blockSize)
|
||||
if size > 32 {
|
||||
return nil, fmt.Errorf("cannot read paletted storage (size=%v) %T: size too large", blockSize, pe)
|
||||
}
|
||||
uint32Count := size.uint32s()
|
||||
|
||||
uint32s := make([]uint32, uint32Count)
|
||||
byteCount := uint32Count * 4
|
||||
|
||||
data := buf.Next(byteCount)
|
||||
if len(data) != byteCount {
|
||||
return nil, fmt.Errorf("cannot read paletted storage (size=%v) %T: not enough block data present: expected %v bytes, got %v", blockSize, pe, byteCount, len(data))
|
||||
}
|
||||
for i := 0; i < uint32Count; i++ {
|
||||
// Explicitly don't use the binary package to greatly improve performance of reading the uint32s.
|
||||
uint32s[i] = uint32(data[i*4]) | uint32(data[i*4+1])<<8 | uint32(data[i*4+2])<<16 | uint32(data[i*4+3])<<24
|
||||
}
|
||||
p, err := e.decodePalette(buf, paletteSize(blockSize), pe)
|
||||
return newPalettedStorage(uint32s, p), err
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// SubChunkVersion is the current version of the written sub chunks, specifying the format they are
|
||||
// written on disk and over network.
|
||||
SubChunkVersion = 9
|
||||
// CurrentBlockVersion is the current version of blocks (states) of the game. This version is composed
|
||||
// of 4 bytes indicating a version, interpreted as a big endian int. The current version represents
|
||||
// 1.16.0.14 {1, 16, 0, 14}.
|
||||
CurrentBlockVersion int32 = 18040335
|
||||
)
|
||||
|
||||
var (
|
||||
// pool is used to pool byte buffers used for encoding chunks.
|
||||
pool = sync.Pool{
|
||||
New: func() any {
|
||||
return bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
// SerialisedData holds the serialised data of a chunk. It consists of the chunk's block data itself, a height
|
||||
// map, the biomes and entities and block entities.
|
||||
SerialisedData struct {
|
||||
// sub holds the data of the serialised sub chunks in a chunk. Sub chunks that are empty or that otherwise
|
||||
// don't exist are represented as an empty slice (or technically, nil).
|
||||
SubChunks [][]byte
|
||||
// Biomes is the biome data of the chunk, which is composed of a biome storage for each sub-chunk.
|
||||
Biomes []byte
|
||||
}
|
||||
// blockEntry represents a block as found in a disk save of a world.
|
||||
blockEntry struct {
|
||||
Name string `nbt:"name"`
|
||||
State map[string]any `nbt:"states"`
|
||||
Version int32 `nbt:"version"`
|
||||
}
|
||||
)
|
||||
|
||||
// Encode encodes Chunk to an intermediate representation SerialisedData. An Encoding may be passed to encode either for
|
||||
// network or disk purposed, the most notable difference being that the network encoding generally uses varints and no
|
||||
// NBT.
|
||||
func Encode(c *Chunk, e Encoding) SerialisedData {
|
||||
d := SerialisedData{SubChunks: make([][]byte, len(c.sub))}
|
||||
for i := range c.sub {
|
||||
d.SubChunks[i] = EncodeSubChunk(c, e, i)
|
||||
}
|
||||
d.Biomes = EncodeBiomes(c, e)
|
||||
return d
|
||||
}
|
||||
|
||||
// EncodeSubChunk encodes a sub-chunk from a chunk into bytes. An Encoding may be passed to encode either for network or
|
||||
// disk purposed, the most notable difference being that the network encoding generally uses varints and no NBT.
|
||||
func EncodeSubChunk(c *Chunk, e Encoding, ind int) []byte {
|
||||
buf := pool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
pool.Put(buf)
|
||||
}()
|
||||
|
||||
s := c.sub[ind]
|
||||
_, _ = buf.Write([]byte{SubChunkVersion, byte(len(s.storages)), uint8(ind + (c.r[0] >> 4))})
|
||||
for _, storage := range s.storages {
|
||||
encodePalettedStorage(buf, storage, nil, e, BlockPaletteEncoding{Blocks: c.br})
|
||||
}
|
||||
sub := make([]byte, buf.Len())
|
||||
_, _ = buf.Read(sub)
|
||||
return sub
|
||||
}
|
||||
|
||||
// EncodeBiomes encodes the biomes of a chunk into bytes. An Encoding may be passed to encode either for network or
|
||||
// disk purposed, the most notable difference being that the network encoding generally uses varints and no NBT.
|
||||
func EncodeBiomes(c *Chunk, e Encoding) []byte {
|
||||
buf := pool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
pool.Put(buf)
|
||||
}()
|
||||
|
||||
var previous *PalettedStorage
|
||||
for _, b := range c.biomes {
|
||||
encodePalettedStorage(buf, b, previous, e, BiomePaletteEncoding)
|
||||
previous = b
|
||||
}
|
||||
biomes := make([]byte, buf.Len())
|
||||
_, _ = buf.Read(biomes)
|
||||
return biomes
|
||||
}
|
||||
|
||||
// encodePalettedStorage encodes a PalettedStorage into a bytes.Buffer. The Encoding passed is used to write the Palette
|
||||
// of the PalettedStorage.
|
||||
func encodePalettedStorage(buf *bytes.Buffer, storage, previous *PalettedStorage, e Encoding, pe paletteEncoding) {
|
||||
if storage.Equal(previous) {
|
||||
_, _ = buf.Write([]byte{0x7f<<1 | e.network()})
|
||||
return
|
||||
}
|
||||
b := make([]byte, len(storage.indices)*4+1)
|
||||
b[0] = byte(storage.bitsPerIndex<<1) | e.network()
|
||||
|
||||
for i, v := range storage.indices {
|
||||
// Explicitly don't use the binary package to greatly improve performance of writing the uint32s.
|
||||
b[i*4+1], b[i*4+2], b[i*4+3], b[i*4+4] = byte(v), byte(v>>8), byte(v>>16), byte(v>>24)
|
||||
}
|
||||
_, _ = buf.Write(b)
|
||||
|
||||
e.encodePalette(buf, storage.palette, pe)
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/df-mc/worldupgrader/blockupgrader"
|
||||
"github.com/sandertv/gophertunnel/minecraft/nbt"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
)
|
||||
|
||||
type (
|
||||
// Encoding is an encoding type used for Chunk encoding. Implementations of this interface are DiskEncoding and
|
||||
// NetworkEncoding, which can be used to encode a Chunk to an intermediate disk or network representation respectively.
|
||||
Encoding interface {
|
||||
encodePalette(buf *bytes.Buffer, p *Palette, e paletteEncoding)
|
||||
decodePalette(buf *bytes.Buffer, blockSize paletteSize, e paletteEncoding) (*Palette, error)
|
||||
network() byte
|
||||
}
|
||||
// paletteEncoding is an encoding type used for Chunk encoding. It is used to encode different types of palettes
|
||||
// (for example, blocks or biomes) differently.
|
||||
paletteEncoding interface {
|
||||
encode(buf *bytes.Buffer, v uint32)
|
||||
decode(buf *bytes.Buffer) (uint32, error)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// DiskEncoding is the Encoding for writing a Chunk to disk. It writes block palettes using NBT and does not use
|
||||
// varints.
|
||||
DiskEncoding diskEncoding
|
||||
// NetworkEncoding is the Encoding used for sending a Chunk over network. It does not use NBT and writes varints.
|
||||
NetworkEncoding networkEncoding
|
||||
// BiomePaletteEncoding is the paletteEncoding used for encoding a palette of biomes.
|
||||
BiomePaletteEncoding biomePaletteEncoding
|
||||
)
|
||||
|
||||
// biomePaletteEncoding implements the encoding of biome palettes to disk.
|
||||
type biomePaletteEncoding struct{}
|
||||
|
||||
func (biomePaletteEncoding) encode(buf *bytes.Buffer, v uint32) {
|
||||
_ = binary.Write(buf, binary.LittleEndian, v)
|
||||
}
|
||||
func (biomePaletteEncoding) decode(buf *bytes.Buffer) (uint32, error) {
|
||||
var v uint32
|
||||
return v, binary.Read(buf, binary.LittleEndian, &v)
|
||||
}
|
||||
|
||||
// BlockPaletteEncoding implements the encoding of block palettes to disk. It requires a BlockRegistry for converting
|
||||
// between runtime IDs and block states.
|
||||
type BlockPaletteEncoding struct {
|
||||
Blocks BlockRegistry
|
||||
}
|
||||
|
||||
func (bpe BlockPaletteEncoding) encode(buf *bytes.Buffer, v uint32) {
|
||||
_ = nbt.NewEncoderWithEncoding(buf, nbt.LittleEndian).Encode(bpe.EncodeBlockState(v))
|
||||
}
|
||||
func (bpe BlockPaletteEncoding) decode(buf *bytes.Buffer) (uint32, error) {
|
||||
var m map[string]any
|
||||
if err := nbt.NewDecoderWithEncoding(buf, nbt.LittleEndian).Decode(&m); err != nil {
|
||||
return 0, fmt.Errorf("error decoding block palette entry: %w", err)
|
||||
}
|
||||
return bpe.DecodeBlockState(m)
|
||||
}
|
||||
|
||||
func (bpe BlockPaletteEncoding) EncodeBlockState(v uint32) blockEntry {
|
||||
// Get the block state registered with the runtime IDs we have in the palette of the block storage
|
||||
// as we need the name and data value to store.
|
||||
name, props, _ := bpe.Blocks.RuntimeIDToState(v)
|
||||
return blockEntry{Name: name, State: props, Version: CurrentBlockVersion}
|
||||
}
|
||||
|
||||
func (bpe BlockPaletteEncoding) DecodeBlockState(m map[string]any) (uint32, error) {
|
||||
// Decode the name and version of the block entry.
|
||||
name, _ := m["name"].(string)
|
||||
version, _ := m["version"].(int32)
|
||||
|
||||
// Now check for a state field.
|
||||
stateI, ok := m["states"]
|
||||
if version < 17694723 {
|
||||
// This entry is a pre-1.13 block state, so decode the meta value instead.
|
||||
meta, _ := m["val"].(int16)
|
||||
|
||||
// Upgrade the pre-1.13 state into a post-1.13 state.
|
||||
state, ok := upgradeLegacyEntry(name, meta)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("cannot find mapping for legacy block entry: %v, %v", name, meta)
|
||||
}
|
||||
|
||||
// Update the name, state, and version.
|
||||
name = state.Name
|
||||
stateI = state.State
|
||||
version = state.Version
|
||||
} else if !ok {
|
||||
// The state is a post-1.13 block state, but the states field is missing, likely due to a broken world
|
||||
// conversion.
|
||||
stateI = make(map[string]any)
|
||||
}
|
||||
state, ok := stateI.(map[string]any)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("invalid state in block entry")
|
||||
}
|
||||
|
||||
// Upgrade the block state if necessary.
|
||||
upgraded := blockupgrader.Upgrade(blockupgrader.BlockState{
|
||||
Name: name,
|
||||
Properties: state,
|
||||
Version: version,
|
||||
})
|
||||
|
||||
v, ok := bpe.Blocks.StateToRuntimeID(upgraded.Name, upgraded.Properties)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("cannot get runtime ID of block state %v{%+v} %v", upgraded.Name, upgraded.Properties, upgraded.Version)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// diskEncoding implements the Chunk encoding for writing to disk.
|
||||
type diskEncoding struct{}
|
||||
|
||||
func (diskEncoding) network() byte { return 0 }
|
||||
func (diskEncoding) encodePalette(buf *bytes.Buffer, p *Palette, e paletteEncoding) {
|
||||
if p.size != 0 {
|
||||
_ = binary.Write(buf, binary.LittleEndian, uint32(p.Len()))
|
||||
}
|
||||
for _, v := range p.values {
|
||||
e.encode(buf, v)
|
||||
}
|
||||
}
|
||||
func (diskEncoding) decodePalette(buf *bytes.Buffer, blockSize paletteSize, e paletteEncoding) (*Palette, error) {
|
||||
paletteCount := uint32(1)
|
||||
if blockSize != 0 {
|
||||
if err := binary.Read(buf, binary.LittleEndian, &paletteCount); err != nil {
|
||||
return nil, fmt.Errorf("error reading palette entry count: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
palette := newPalette(blockSize, make([]uint32, paletteCount))
|
||||
for i := uint32(0); i < paletteCount; i++ {
|
||||
palette.values[i], err = e.decode(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if paletteCount == 0 {
|
||||
return palette, fmt.Errorf("invalid palette entry count: found 0, but palette with %v bits per block must have at least 1 value", blockSize)
|
||||
}
|
||||
return palette, nil
|
||||
}
|
||||
|
||||
// networkEncoding implements the Chunk encoding for sending over network.
|
||||
type networkEncoding struct{}
|
||||
|
||||
func (networkEncoding) network() byte { return 1 }
|
||||
func (networkEncoding) encodePalette(buf *bytes.Buffer, p *Palette, _ paletteEncoding) {
|
||||
if p.size != 0 {
|
||||
_ = protocol.WriteVarint32(buf, int32(p.Len()))
|
||||
}
|
||||
for _, val := range p.values {
|
||||
_ = protocol.WriteVarint32(buf, int32(val))
|
||||
}
|
||||
}
|
||||
func (networkEncoding) decodePalette(buf *bytes.Buffer, blockSize paletteSize, _ paletteEncoding) (*Palette, error) {
|
||||
var paletteCount int32 = 1
|
||||
if blockSize != 0 {
|
||||
if err := protocol.Varint32(buf, &paletteCount); err != nil {
|
||||
return nil, fmt.Errorf("error reading palette entry count: %w", err)
|
||||
}
|
||||
if paletteCount <= 0 {
|
||||
return nil, fmt.Errorf("invalid palette entry count %v", paletteCount)
|
||||
}
|
||||
}
|
||||
|
||||
blocks, temp := make([]uint32, paletteCount), int32(0)
|
||||
for i := int32(0); i < paletteCount; i++ {
|
||||
if err := protocol.Varint32(buf, &temp); err != nil {
|
||||
return nil, fmt.Errorf("error decoding palette entry: %w", err)
|
||||
}
|
||||
blocks[i] = uint32(temp)
|
||||
}
|
||||
return &Palette{values: blocks, size: blockSize}, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
// HeightMap represents the height map of a chunk. It holds the y value of all the highest blocks in the chunk
|
||||
// that diffuse or obstruct light.
|
||||
type HeightMap []int16
|
||||
|
||||
// At returns the height map value at a specific column in the chunk.
|
||||
func (h HeightMap) At(x, z uint8) int16 {
|
||||
return h[(uint16(x)<<4)|uint16(z)]
|
||||
}
|
||||
|
||||
// Set changes the height map value at a specific column in the chunk.
|
||||
func (h HeightMap) Set(x, z uint8, val int16) {
|
||||
h[(uint16(x)<<4)|uint16(z)] = val
|
||||
}
|
||||
|
||||
// HighestNeighbour returns the height map value of the highest directly neighbouring column of the x and z values
|
||||
// passed.
|
||||
func (h HeightMap) HighestNeighbour(x, z uint8) int16 {
|
||||
highest := int16(math.MinInt16)
|
||||
if x != 15 {
|
||||
if val := h.At(x+1, z); val > highest {
|
||||
highest = val
|
||||
}
|
||||
}
|
||||
if x != 0 {
|
||||
if val := h.At(x-1, z); val > highest {
|
||||
highest = val
|
||||
}
|
||||
}
|
||||
if z != 15 {
|
||||
if val := h.At(x, z+1); val > highest {
|
||||
highest = val
|
||||
}
|
||||
}
|
||||
if z != 0 {
|
||||
if val := h.At(x, z-1); val > highest {
|
||||
highest = val
|
||||
}
|
||||
}
|
||||
return highest
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,131 @@
|
||||
package chunk
|
||||
|
||||
import "github.com/df-mc/dragonfly/server/block/cube"
|
||||
|
||||
// insertBlockLightNodes iterates over the chunk and looks for blocks that have a light level of at least 1.
|
||||
// If one is found, a node is added for it to the node queue.
|
||||
func (a *lightArea) insertBlockLightNodes(queue *lightQueue) {
|
||||
a.iterSubChunks(a.anyLightBlocks, func(pos cube.Pos) {
|
||||
if level := a.highest(pos, a.br.LightBlock); level > 0 {
|
||||
queue.push(node(pos, level, BlockLight))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// anyLightBlocks checks if there are any blocks in the SubChunk passed that emit light.
|
||||
func (a *lightArea) anyLightBlocks(sub *SubChunk) bool {
|
||||
for _, layer := range sub.storages {
|
||||
for _, id := range layer.palette.values {
|
||||
if a.br.LightBlock(id) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// insertSkyLightNodes iterates over the chunk and inserts a light node anywhere at the highest block in the
|
||||
// chunk. In addition, any skylight above those nodes will be set to 15.
|
||||
func (a *lightArea) insertSkyLightNodes(queue *lightQueue) {
|
||||
a.iterHeightmap(func(x, z int, height, highestNeighbour, highestY, lowestY int) {
|
||||
pos := cube.Pos{x, height, z}
|
||||
if height <= a.r.Max() {
|
||||
// Only set light if we're not at the top of the world.
|
||||
a.setLight(pos, SkyLight, 15)
|
||||
|
||||
if pos[1] > lowestY {
|
||||
if level := a.highest(pos.Sub(cube.Pos{0, 1}), a.br.FilteringBlock); level != 15 && level != 0 {
|
||||
// If we hit a block like water or leaves (something that diffuses but does not block light), we
|
||||
// need a node above this block regardless of the neighbours.
|
||||
queue.push(node(pos, 15, SkyLight))
|
||||
}
|
||||
}
|
||||
}
|
||||
for y := pos[1]; y < highestY; y++ {
|
||||
// We can do a bit of an optimisation here: We don't need to insert nodes if the neighbours are
|
||||
// lower than the current one, on the same Y level, or one level higher, because light in
|
||||
// this column can't spread below that anyway.
|
||||
if pos[1]++; pos[1] < highestNeighbour {
|
||||
queue.push(node(pos, 15, SkyLight))
|
||||
continue
|
||||
}
|
||||
// Fill the rest with full skylight.
|
||||
a.setLight(pos, SkyLight, 15)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// insertLightSpreadingNodes inserts light nodes into the node queue passed which, when propagated, will
|
||||
// spread into the neighbouring chunks.
|
||||
func (a *lightArea) insertLightSpreadingNodes(queue *lightQueue, lt light) {
|
||||
a.iterEdges(a.nodesNeeded(lt), func(pa, pb cube.Pos) {
|
||||
la, lb := a.light(pa, lt), a.light(pb, lt)
|
||||
if la == lb || la-1 == lb || lb-1 == la {
|
||||
// No chance for this to spread. Don't check for the highest filtering blocks on the side.
|
||||
return
|
||||
}
|
||||
if filter := a.highest(pb, a.br.FilteringBlock) + 1; la > filter && la-filter > lb {
|
||||
queue.push(node(pb, la-filter, lt))
|
||||
} else if filter = a.highest(pa, a.br.FilteringBlock) + 1; lb > filter && lb-filter > la {
|
||||
queue.push(node(pa, lb-filter, lt))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// nodesNeeded checks if any light nodes of a specific light type are needed between two neighbouring SubChunks when
|
||||
// spreading light between them.
|
||||
func (a *lightArea) nodesNeeded(lt light) func(sa, sb *SubChunk) bool {
|
||||
if lt == SkyLight {
|
||||
return func(sa, sb *SubChunk) bool {
|
||||
return &sa.skyLight[0] != &sb.skyLight[0]
|
||||
}
|
||||
}
|
||||
return func(sa, sb *SubChunk) bool {
|
||||
// Don't add nodes if both sub chunks are either both fully filled with light or have no light at all.
|
||||
return &sa.blockLight[0] != &sb.blockLight[0]
|
||||
}
|
||||
}
|
||||
|
||||
// propagate spreads the next light node in the node queue passed through the lightArea a. propagate adds the neighbours
|
||||
// of the node to the queue for as long as it is able to spread.
|
||||
func (a *lightArea) propagate(queue *lightQueue) {
|
||||
n, ok := queue.pop()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if a.light(n.pos, n.lt) >= n.level {
|
||||
return
|
||||
}
|
||||
a.setLight(n.pos, n.lt, n.level)
|
||||
|
||||
x, y, z := n.pos[0], n.pos[1], n.pos[2]
|
||||
a.propagateNeighbour(queue, n.lt, n.level, x+1, y, z)
|
||||
a.propagateNeighbour(queue, n.lt, n.level, x-1, y, z)
|
||||
a.propagateNeighbour(queue, n.lt, n.level, x, y+1, z)
|
||||
a.propagateNeighbour(queue, n.lt, n.level, x, y-1, z)
|
||||
a.propagateNeighbour(queue, n.lt, n.level, x, y, z+1)
|
||||
a.propagateNeighbour(queue, n.lt, n.level, x, y, z-1)
|
||||
}
|
||||
|
||||
func (a *lightArea) propagateNeighbour(queue *lightQueue, lt light, level uint8, x, y, z int) {
|
||||
if y < a.r.Min() || y > a.r.Max() || x < a.baseX || z < a.baseZ || x >= a.baseX+a.w*16 || z >= a.baseZ+a.w*16 {
|
||||
return
|
||||
}
|
||||
pos := cube.Pos{x, y, z}
|
||||
filter := a.highest(pos, a.br.FilteringBlock) + 1
|
||||
if level > filter && a.light(pos, lt) < level-filter {
|
||||
queue.push(node(pos, level-filter, lt))
|
||||
}
|
||||
}
|
||||
|
||||
// lightNode is a node pushed to the queue which is used to propagate light.
|
||||
type lightNode struct {
|
||||
pos cube.Pos
|
||||
lt light
|
||||
level uint8
|
||||
}
|
||||
|
||||
// node creates a new lightNode using the position, level and light type passed.
|
||||
func node(pos cube.Pos, level uint8, lt light) lightNode {
|
||||
return lightNode{pos: pos, level: level, lt: lt}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package chunk
|
||||
|
||||
var (
|
||||
// SkyLight holds a light implementation that can be used for propagating sky light through a sub chunk.
|
||||
SkyLight skyLight
|
||||
// BlockLight holds a light implementation that can be used for propagating block light through a sub chunk.
|
||||
BlockLight blockLight
|
||||
)
|
||||
|
||||
type (
|
||||
// light is a type that can be used to set and retrieve light of a specific type in a sub chunk.
|
||||
light interface {
|
||||
light(sub *SubChunk, x, y, z uint8) uint8
|
||||
setLight(sub *SubChunk, x, y, z, v uint8)
|
||||
}
|
||||
skyLight struct{}
|
||||
blockLight struct{}
|
||||
)
|
||||
|
||||
func (skyLight) light(sub *SubChunk, x, y, z uint8) uint8 { return sub.SkyLight(x, y, z) }
|
||||
func (skyLight) setLight(sub *SubChunk, x, y, z, v uint8) { sub.SetSkyLight(x, y, z, v) }
|
||||
func (blockLight) light(sub *SubChunk, x, y, z uint8) uint8 { return sub.BlockLight(x, y, z) }
|
||||
func (blockLight) setLight(sub *SubChunk, x, y, z, v uint8) { sub.SetBlockLight(x, y, z, v) }
|
||||
@@ -0,0 +1,142 @@
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// paletteSize is the size of a palette. It indicates the amount of bits occupied per value stored.
|
||||
type paletteSize byte
|
||||
|
||||
// Palette is a palette of values that every PalettedStorage has. Storages hold 'pointers' to indices
|
||||
// in this palette.
|
||||
type Palette struct {
|
||||
last uint32
|
||||
lastIndex int16
|
||||
size paletteSize
|
||||
|
||||
// values is a map of values. A PalettedStorage points to the index to this value.
|
||||
values []uint32
|
||||
}
|
||||
|
||||
// newPalette returns a new Palette with size and a slice of added values.
|
||||
func newPalette(size paletteSize, values []uint32) *Palette {
|
||||
return &Palette{size: size, values: values, last: math.MaxUint32}
|
||||
}
|
||||
|
||||
// Clone returns an independent copy of the Palette.
|
||||
func (palette *Palette) Clone() *Palette {
|
||||
return &Palette{
|
||||
last: palette.last,
|
||||
lastIndex: palette.lastIndex,
|
||||
size: palette.size,
|
||||
values: slices.Clone(palette.values),
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the amount of unique values in the Palette.
|
||||
func (palette *Palette) Len() int {
|
||||
return len(palette.values)
|
||||
}
|
||||
|
||||
// Add adds a values to the Palette. It does not first check if the value was already set in the Palette.
|
||||
// The index at which the value was added is returned. Another bool is returned indicating if the Palette
|
||||
// was resized as a result of adding the value.
|
||||
func (palette *Palette) Add(v uint32) (index int16, resize bool) {
|
||||
i := int16(len(palette.values))
|
||||
palette.values = append(palette.values, v)
|
||||
|
||||
if palette.needsResize() {
|
||||
palette.increaseSize()
|
||||
return i, true
|
||||
}
|
||||
return i, false
|
||||
}
|
||||
|
||||
// Replace calls the function passed for each value present in the Palette. The value returned by the
|
||||
// function replaces the value present at the index of the value passed.
|
||||
func (palette *Palette) Replace(f func(v uint32) uint32) {
|
||||
// Reset last runtime ID as it now has a different offset.
|
||||
palette.last = math.MaxUint32
|
||||
for index, v := range palette.values {
|
||||
palette.values[index] = f(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Index loops through the values of the Palette and looks for the index of the given value. If the value could
|
||||
// not be found, -1 is returned.
|
||||
func (palette *Palette) Index(runtimeID uint32) int16 {
|
||||
if runtimeID == palette.last {
|
||||
// Fast path out.
|
||||
return palette.lastIndex
|
||||
}
|
||||
// Slow path in a separate function allows for inlining the fast path.
|
||||
return palette.indexSlow(runtimeID)
|
||||
}
|
||||
|
||||
// indexSlow searches the index of a value in the Palette's values by iterating through the Palette's values.
|
||||
func (palette *Palette) indexSlow(runtimeID uint32) int16 {
|
||||
l := len(palette.values)
|
||||
for i := 0; i < l; i++ {
|
||||
if palette.values[i] == runtimeID {
|
||||
palette.last = runtimeID
|
||||
v := int16(i)
|
||||
palette.lastIndex = v
|
||||
return v
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Value returns the value in the Palette at a specific index.
|
||||
func (palette *Palette) Value(i uint16) uint32 {
|
||||
return palette.values[i]
|
||||
}
|
||||
|
||||
// needsResize checks if the Palette, and with it the holding PalettedStorage, needs to be resized to a bigger
|
||||
// size.
|
||||
func (palette *Palette) needsResize() bool {
|
||||
return len(palette.values) > (1 << palette.size)
|
||||
}
|
||||
|
||||
var sizes = [...]paletteSize{0, 1, 2, 3, 4, 5, 6, 8, 16}
|
||||
var offsets = [...]int{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 8: 7, 16: 8}
|
||||
|
||||
// increaseSize increases the size of the Palette to the next palette size.
|
||||
func (palette *Palette) increaseSize() {
|
||||
palette.size = sizes[offsets[palette.size]+1]
|
||||
}
|
||||
|
||||
// padded returns true if the Palette size is 3, 5 or 6.
|
||||
func (p paletteSize) padded() bool {
|
||||
return p == 3 || p == 5 || p == 6
|
||||
}
|
||||
|
||||
// paletteSizeFor finds a suitable paletteSize for the amount of values passed n.
|
||||
func paletteSizeFor(n int) paletteSize {
|
||||
for _, size := range sizes {
|
||||
if n <= (1 << size) {
|
||||
return size
|
||||
}
|
||||
}
|
||||
// Should never happen.
|
||||
return 0
|
||||
}
|
||||
|
||||
// uint32s returns the amount of uint32s needed to represent a storage with this palette size.
|
||||
func (p paletteSize) uint32s() (n int) {
|
||||
uint32Count := 0
|
||||
if p != 0 {
|
||||
// indicesPerUint32 is the amount of indices that may be stored in a single uint32.
|
||||
indicesPerUint32 := 32 / int(p)
|
||||
// uint32Count is the amount of uint32s required to store all indices: 4096 indices need to be stored in
|
||||
// total.
|
||||
uint32Count = 4096 / indicesPerUint32
|
||||
}
|
||||
if p.padded() {
|
||||
// We've got one of the padded sizes, so the storage has another uint32 to be able to store
|
||||
// every index.
|
||||
uint32Count++
|
||||
}
|
||||
return uint32Count
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package chunk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"slices"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// uint32ByteSize is the amount of bytes in a uint32.
|
||||
uint32ByteSize = 4
|
||||
// uint32BitSize is the amount of bits in a uint32.
|
||||
uint32BitSize = uint32ByteSize * 8
|
||||
)
|
||||
|
||||
// PalettedStorage is a storage of 4096 blocks encoded in a variable amount of uint32s, storages may have values
|
||||
// with a bit size per block of 0, 1, 2, 3, 4, 5, 6, 8 or 16 bits.
|
||||
// 3 of these formats have additional padding in every uint32 and an additional uint32 at the end, to cater
|
||||
// for the blocks that don't fit. This padding is present when the storage has a block size of 3, 5 or 6
|
||||
// bytes.
|
||||
// Methods on PalettedStorage must not be called simultaneously from multiple goroutines.
|
||||
type PalettedStorage struct {
|
||||
// bitsPerIndex is the amount of bits required to store one block. The number increases as the block
|
||||
// storage holds more unique block states.
|
||||
bitsPerIndex uint16
|
||||
// filledBitsPerIndex returns the amount of blocks that are actually filled per uint32.
|
||||
filledBitsPerIndex uint16
|
||||
// indexMask is the equivalent of 1 << bitsPerIndex - 1.
|
||||
indexMask uint32
|
||||
|
||||
// indicesStart holds an unsafe.Pointer to the first byte in the indices slice below.
|
||||
indicesStart unsafe.Pointer
|
||||
|
||||
// Palette holds all block runtime IDs that the indices in the indices slice point to. These runtime IDs
|
||||
// point to block states.
|
||||
palette *Palette
|
||||
|
||||
// indices contains all indices in the PalettedStorage. This slice has a variable size, but may not be changed
|
||||
// unless the whole PalettedStorage is resized, including the Palette.
|
||||
indices []uint32
|
||||
}
|
||||
|
||||
// newPalettedStorage creates a new block storage using the uint32 slice as the indices and the palette passed.
|
||||
// The bits per block are calculated using the length of the uint32 slice.
|
||||
func newPalettedStorage(indices []uint32, palette *Palette) *PalettedStorage {
|
||||
var (
|
||||
bitsPerIndex = uint16(len(indices) / uint32BitSize / uint32ByteSize)
|
||||
indexMask = (uint32(1) << bitsPerIndex) - 1
|
||||
indicesStart = (unsafe.Pointer)(unsafe.SliceData(indices))
|
||||
filledBitsPerIndex uint16
|
||||
)
|
||||
if bitsPerIndex != 0 {
|
||||
filledBitsPerIndex = uint32BitSize / bitsPerIndex * bitsPerIndex
|
||||
}
|
||||
return &PalettedStorage{filledBitsPerIndex: filledBitsPerIndex, indexMask: indexMask, indicesStart: indicesStart, bitsPerIndex: bitsPerIndex, indices: indices, palette: palette}
|
||||
}
|
||||
|
||||
// emptyStorage creates a PalettedStorage filled completely with a value v.
|
||||
func emptyStorage(v uint32) *PalettedStorage {
|
||||
return newPalettedStorage([]uint32{}, newPalette(0, []uint32{v}))
|
||||
}
|
||||
|
||||
// Clone returns an independent copy of the PalettedStorage.
|
||||
func (storage *PalettedStorage) Clone() *PalettedStorage {
|
||||
return newPalettedStorage(slices.Clone(storage.indices), storage.palette.Clone())
|
||||
}
|
||||
|
||||
// Palette returns the Palette of the PalettedStorage.
|
||||
func (storage *PalettedStorage) Palette() *Palette {
|
||||
return storage.palette
|
||||
}
|
||||
|
||||
// At returns the value of the PalettedStorage at a given x, y and z.
|
||||
func (storage *PalettedStorage) At(x, y, z byte) uint32 {
|
||||
return storage.palette.Value(storage.paletteIndex(x&15, y&15, z&15))
|
||||
}
|
||||
|
||||
// Set sets a value at a specific x, y and z. The Palette and PalettedStorage are expanded
|
||||
// automatically to make space for the value, should that be needed.
|
||||
func (storage *PalettedStorage) Set(x, y, z byte, v uint32) {
|
||||
index := storage.palette.Index(v)
|
||||
if index == -1 {
|
||||
// The runtime ID was not yet available in the palette. We add it, then check if the block storage
|
||||
// needs to be resized for the palette pointers to fit.
|
||||
index = storage.addNew(v)
|
||||
}
|
||||
storage.setPaletteIndex(x&15, y&15, z&15, uint16(index))
|
||||
}
|
||||
|
||||
// Equal checks if two PalettedStorages are equal value wise. False is returned
|
||||
// if either of the storages are nil.
|
||||
func (storage *PalettedStorage) Equal(other *PalettedStorage) bool {
|
||||
if storage == nil || other == nil {
|
||||
return false
|
||||
}
|
||||
if len(storage.indices) == 0 || len(other.indices) == 0 || storage.palette.values[0] == 0 || other.palette.values[0] == 0 {
|
||||
return false
|
||||
}
|
||||
indicesA := unsafe.Slice((*byte)(unsafe.Pointer(&storage.indices[0])), len(storage.indices)*4)
|
||||
indicesB := unsafe.Slice((*byte)(unsafe.Pointer(&other.indices[0])), len(other.indices)*4)
|
||||
if !bytes.Equal(indicesA, indicesB) {
|
||||
return false
|
||||
}
|
||||
paletteA := unsafe.Slice((*byte)(unsafe.Pointer(&storage.palette.values[0])), len(storage.palette.values)*4)
|
||||
paletteB := unsafe.Slice((*byte)(unsafe.Pointer(&other.palette.values[0])), len(other.palette.values)*4)
|
||||
return bytes.Equal(paletteA, paletteB)
|
||||
}
|
||||
|
||||
// addNew adds a new value to the PalettedStorage's Palette and returns its index. If needed, the storage is resized.
|
||||
func (storage *PalettedStorage) addNew(v uint32) int16 {
|
||||
index, resize := storage.palette.Add(v)
|
||||
if resize {
|
||||
storage.resize(storage.palette.size)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// paletteIndex looks up the Palette index at a given x, y and z value in the PalettedStorage. This palette
|
||||
// index is not the value at this offset, but merely an index in the Palette pointing to a value.
|
||||
func (storage *PalettedStorage) paletteIndex(x, y, z byte) uint16 {
|
||||
if storage.bitsPerIndex == 0 {
|
||||
// Unfortunately our default logic cannot deal with 0 bits per index, meaning we'll have to special case
|
||||
// this. This comes with a little performance hit, but it seems to be the only way to go. An alternative would
|
||||
// be not to have 0 bits per block storages in memory, but that would cause a strongly increased memory usage
|
||||
// by biomes.
|
||||
return 0
|
||||
}
|
||||
offset := ((uint16(x) << 8) | (uint16(z) << 4) | uint16(y)) * storage.bitsPerIndex
|
||||
uint32Offset, bitOffset := offset/storage.filledBitsPerIndex, offset%storage.filledBitsPerIndex
|
||||
|
||||
w := *(*uint32)(unsafe.Pointer(uintptr(storage.indicesStart) + uintptr(uint32Offset<<2)))
|
||||
return uint16((w >> bitOffset) & storage.indexMask)
|
||||
}
|
||||
|
||||
// setPaletteIndex sets the palette index at a given x, y and z to paletteIndex. This index should point
|
||||
// to a value in the PalettedStorage's Palette.
|
||||
func (storage *PalettedStorage) setPaletteIndex(x, y, z byte, i uint16) {
|
||||
if storage.bitsPerIndex == 0 {
|
||||
return
|
||||
}
|
||||
offset := ((uint16(x) << 8) | (uint16(z) << 4) | uint16(y)) * storage.bitsPerIndex
|
||||
uint32Offset, bitOffset := offset/storage.filledBitsPerIndex, offset%storage.filledBitsPerIndex
|
||||
|
||||
ptr := (*uint32)(unsafe.Pointer(uintptr(storage.indicesStart) + uintptr(uint32Offset<<2)))
|
||||
*ptr = (*ptr &^ (storage.indexMask << bitOffset)) | (uint32(i) << bitOffset)
|
||||
}
|
||||
|
||||
// resize changes the size of a PalettedStorage to newPaletteSize. A new PalettedStorage is constructed,
|
||||
// and all values available in the current storage are set in their appropriate locations in the
|
||||
// new storage.
|
||||
func (storage *PalettedStorage) resize(newPaletteSize paletteSize) {
|
||||
if newPaletteSize == paletteSize(storage.bitsPerIndex) {
|
||||
return // Don't resize if the size is already equal.
|
||||
}
|
||||
// Construct a new storage and set all values in there manually. We can't easily do this in a better
|
||||
// way, because all values will be at a different index with a different length.
|
||||
newStorage := newPalettedStorage(make([]uint32, newPaletteSize.uint32s()), storage.palette)
|
||||
for x := byte(0); x < 16; x++ {
|
||||
for y := byte(0); y < 16; y++ {
|
||||
for z := byte(0); z < 16; z++ {
|
||||
newStorage.setPaletteIndex(x, y, z, storage.paletteIndex(x, y, z))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set the new storage.
|
||||
*storage = *newStorage
|
||||
}
|
||||
|
||||
// compact clears unused indexes in the palette by scanning for usages in the PalettedStorage. This is a
|
||||
// relatively heavy task which should only happen right before the sub chunk holding this PalettedStorage is
|
||||
// saved to disk. compact also shrinks the palette size if possible.
|
||||
func (storage *PalettedStorage) compact() {
|
||||
if storage.palette.Len() == 0 {
|
||||
return
|
||||
}
|
||||
if storage.palette.Len() == 1 {
|
||||
// A single unique value can always be represented using 0 bits per index. This avoids scanning the
|
||||
// entire storage and drops any backing indices slice.
|
||||
storage.bitsPerIndex = 0
|
||||
storage.filledBitsPerIndex = 0
|
||||
storage.indexMask = 0
|
||||
storage.indicesStart = nil
|
||||
storage.indices = nil
|
||||
storage.palette.size = 0
|
||||
return
|
||||
}
|
||||
|
||||
usedIndices := make([]bool, storage.palette.Len())
|
||||
for x := byte(0); x < 16; x++ {
|
||||
for y := byte(0); y < 16; y++ {
|
||||
for z := byte(0); z < 16; z++ {
|
||||
usedIndices[storage.paletteIndex(x, y, z)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usedCount := 0
|
||||
allUsed := true
|
||||
for _, used := range usedIndices {
|
||||
if used {
|
||||
usedCount++
|
||||
} else {
|
||||
allUsed = false
|
||||
}
|
||||
}
|
||||
|
||||
// If every palette entry is used and the palette size cannot shrink, nothing changes.
|
||||
// This avoids allocating a new indices slice and palette values slice for already-optimal storages.
|
||||
size := paletteSizeFor(usedCount)
|
||||
if allUsed && size == storage.palette.size {
|
||||
return
|
||||
}
|
||||
|
||||
newRuntimeIDs := make([]uint32, 0, usedCount)
|
||||
conversion := make([]uint16, len(usedIndices))
|
||||
for index, used := range usedIndices {
|
||||
if used {
|
||||
conversion[index] = uint16(len(newRuntimeIDs))
|
||||
newRuntimeIDs = append(newRuntimeIDs, storage.palette.values[index])
|
||||
}
|
||||
}
|
||||
// Construct a new storage and set all values in there manually. We can't easily do this in a better
|
||||
// way, because all values will be at a different index with a different length.
|
||||
newStorage := newPalettedStorage(make([]uint32, size.uint32s()), newPalette(size, newRuntimeIDs))
|
||||
|
||||
for x := byte(0); x < 16; x++ {
|
||||
for y := byte(0); y < 16; y++ {
|
||||
for z := byte(0); z < 16; z++ {
|
||||
// Replace all usages of the old palette indexes with the new indexes using the map we
|
||||
// produced earlier.
|
||||
newStorage.setPaletteIndex(x, y, z, conversion[storage.paletteIndex(x, y, z)])
|
||||
}
|
||||
}
|
||||
}
|
||||
*storage = *newStorage
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package chunk
|
||||
|
||||
import "slices"
|
||||
|
||||
// SubChunk is a cube of blocks located in a chunk. It has a size of 16x16x16 blocks and forms part of a stack
|
||||
// that forms a Chunk.
|
||||
type SubChunk struct {
|
||||
air uint32
|
||||
storages []*PalettedStorage
|
||||
blockLight []uint8
|
||||
skyLight []uint8
|
||||
}
|
||||
|
||||
// Equals returns if the sub chunk passed is equal to the current one.
|
||||
func (sub *SubChunk) Equals(s *SubChunk) bool {
|
||||
if s.air != sub.air || len(s.storages) != len(sub.storages) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, st := range s.storages {
|
||||
if !st.Equal(sub.storages[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// NewSubChunk creates a new sub chunk. All sub chunks should be created through this function.
|
||||
func NewSubChunk(air uint32) *SubChunk {
|
||||
return &SubChunk{air: air}
|
||||
}
|
||||
|
||||
// Clone returns an independent copy of the SubChunk.
|
||||
func (sub *SubChunk) Clone() *SubChunk {
|
||||
clone := &SubChunk{
|
||||
air: sub.air,
|
||||
storages: make([]*PalettedStorage, len(sub.storages)),
|
||||
blockLight: cloneLight(sub.blockLight),
|
||||
skyLight: cloneLight(sub.skyLight),
|
||||
}
|
||||
for i, storage := range sub.storages {
|
||||
clone.storages[i] = storage.Clone()
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func cloneLight(light []uint8) []uint8 {
|
||||
if len(light) == 0 {
|
||||
return slices.Clone(light)
|
||||
}
|
||||
switch &light[0] {
|
||||
case noLightPtr:
|
||||
return noLight
|
||||
case fullLightPtr:
|
||||
return fullLight
|
||||
default:
|
||||
return slices.Clone(light)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty checks if the SubChunk is considered empty. This is the case if the SubChunk has 0 block storages or if it has
|
||||
// a single one that is completely filled with air.
|
||||
func (sub *SubChunk) Empty() bool {
|
||||
return len(sub.storages) == 0 || (len(sub.storages) == 1 && len(sub.storages[0].palette.values) == 1 && sub.storages[0].palette.values[0] == sub.air)
|
||||
}
|
||||
|
||||
// Layer returns a certain block storage/layer from a sub chunk. If no storage at the layer exists, the layer
|
||||
// is created, as well as all layers between the current highest layer and the new highest layer.
|
||||
func (sub *SubChunk) Layer(layer uint8) *PalettedStorage {
|
||||
for uint8(len(sub.storages)) <= layer {
|
||||
// Keep appending to storages until the requested layer is achieved. Makes working with new layers
|
||||
// much easier.
|
||||
sub.storages = append(sub.storages, emptyStorage(sub.air))
|
||||
}
|
||||
return sub.storages[layer]
|
||||
}
|
||||
|
||||
// Layers returns all layers in the sub chunk. This method may also return an empty slice.
|
||||
func (sub *SubChunk) Layers() []*PalettedStorage {
|
||||
return sub.storages
|
||||
}
|
||||
|
||||
// Block returns the runtime ID of the block located at the given X, Y and Z. X, Y and Z must be in a
|
||||
// range of 0-15.
|
||||
func (sub *SubChunk) Block(x, y, z byte, layer uint8) uint32 {
|
||||
if uint8(len(sub.storages)) <= layer {
|
||||
return sub.air
|
||||
}
|
||||
return sub.storages[layer].At(x, y, z)
|
||||
}
|
||||
|
||||
// SetBlock sets the given block runtime ID at the given X, Y and Z. X, Y and Z must be in a range of 0-15.
|
||||
func (sub *SubChunk) SetBlock(x, y, z byte, layer uint8, block uint32) {
|
||||
sub.Layer(layer).Set(x, y, z, block)
|
||||
}
|
||||
|
||||
// SetBlockLight sets the block light value at a specific position in the sub chunk.
|
||||
func (sub *SubChunk) SetBlockLight(x, y, z byte, level uint8) {
|
||||
if ptr := &sub.blockLight[0]; ptr == noLightPtr {
|
||||
// Copy the block light as soon as it is changed to create a COW system.
|
||||
sub.blockLight = append([]byte(nil), sub.blockLight...)
|
||||
}
|
||||
index := (uint16(x) << 8) | (uint16(z) << 4) | uint16(y)
|
||||
|
||||
i := index >> 1
|
||||
bit := (index & 1) << 2
|
||||
sub.blockLight[i] = (sub.blockLight[i] & (0xf0 >> bit)) | (level << bit)
|
||||
}
|
||||
|
||||
// BlockLight returns the block light value at a specific value at a specific position in the sub chunk.
|
||||
func (sub *SubChunk) BlockLight(x, y, z byte) uint8 {
|
||||
index := (uint16(x) << 8) | (uint16(z) << 4) | uint16(y)
|
||||
return (sub.blockLight[index>>1] >> ((index & 1) << 2)) & 0xf
|
||||
}
|
||||
|
||||
// SetSkyLight sets the skylight value at a specific position in the sub chunk.
|
||||
func (sub *SubChunk) SetSkyLight(x, y, z byte, level uint8) {
|
||||
if ptr := &sub.skyLight[0]; ptr == fullLightPtr || ptr == noLightPtr {
|
||||
// Copy the skylight as soon as it is changed to create a COW system.
|
||||
sub.skyLight = append([]byte(nil), sub.skyLight...)
|
||||
}
|
||||
index := (uint16(x) << 8) | (uint16(z) << 4) | uint16(y)
|
||||
|
||||
i := index >> 1
|
||||
bit := (index & 1) << 2
|
||||
sub.skyLight[i] = (sub.skyLight[i] & (0xf0 >> bit)) | (level << bit)
|
||||
}
|
||||
|
||||
// SkyLight returns the skylight value at a specific value at a specific position in the sub chunk.
|
||||
func (sub *SubChunk) SkyLight(x, y, z byte) uint8 {
|
||||
index := (uint16(x) << 8) | (uint16(z) << 4) | uint16(y)
|
||||
return (sub.skyLight[index>>1] >> ((index & 1) << 2)) & 0xf
|
||||
}
|
||||
|
||||
// Compact cleans the garbage from all block storages that sub chunk contains, so that they may be
|
||||
// cleanly written to a database.
|
||||
func (sub *SubChunk) compact() {
|
||||
newStorages := make([]*PalettedStorage, 0, len(sub.storages))
|
||||
for _, storage := range sub.storages {
|
||||
storage.compact()
|
||||
if len(storage.palette.values) == 1 && storage.palette.values[0] == sub.air {
|
||||
// If the palette has only air in it, it means the storage is empty, so we can ignore it.
|
||||
continue
|
||||
}
|
||||
newStorages = append(newStorages, storage)
|
||||
}
|
||||
sub.storages = newStorages
|
||||
}
|
||||
Reference in New Issue
Block a user