Major update of chunk system for 1.16.2

This commit is contained in:
Tom
2020-09-14 20:03:57 -07:00
parent d3bb141fcd
commit eec9d30795
6 changed files with 279 additions and 233 deletions

38
bot/world/bitarray.go Normal file
View File

@ -0,0 +1,38 @@
package world
// bitArray implements a bitfield array where values are packed into uint64
// values. If the next value does not fit into remaining space, the remaining
// space of a uint64 is unused.
type bitArray struct {
width uint // bit width of each value
valsPerElement uint // number of values which fit into a single uint64.
data []uint64
}
// Size returns the number of elements that can fit into the bit array.
func (b *bitArray) Size() int {
return int(b.valsPerElement) * len(b.data)
}
func (b *bitArray) Set(idx, val uint) {
var (
arrayIdx = idx / b.valsPerElement
startBit = (idx % b.valsPerElement) * b.width
mask = ^uint64((1<<b.width - 1) << startBit) // set for all bits except target
)
b.data[arrayIdx] = (b.data[arrayIdx] & mask) | uint64(val<<startBit)
}
func (b *bitArray) Get(idx uint) uint {
var (
arrayIdx = idx / b.valsPerElement
offset = (idx % b.valsPerElement) * b.width
mask = uint64((1<<b.width - 1) << offset) // set for just the target
)
return uint(b.data[arrayIdx]&mask) >> offset
}
func valsPerBitArrayElement(bitsPerValue uint) uint {
return uint(64 / bitsPerValue)
}

View File

@ -0,0 +1,50 @@
package world
import (
"encoding/binary"
"encoding/hex"
"testing"
)
func TestBitArrayBasic(t *testing.T) {
a := bitArray{
width: 5,
valsPerElement: valsPerBitArrayElement(5),
data: make([]uint64, 5),
}
if got, want := a.Size(), 12*5; got != want {
t.Errorf("size = %d, want %d", got, want)
}
a.Set(0, 4)
if v := a.Get(0); v != 4 {
t.Errorf("v[0] = %d, want 4", v)
}
a.Set(12, 8)
if v := a.Get(12); v != 8 {
t.Errorf("v[12] = %d, want 8", v)
}
}
func TestBitArrayHardcoded(t *testing.T) {
d1, _ := hex.DecodeString("0020863148418841")
d2, _ := hex.DecodeString("01018A7260F68C87")
a := bitArray{
width: 5,
valsPerElement: valsPerBitArrayElement(5),
data: []uint64{binary.BigEndian.Uint64(d1), binary.BigEndian.Uint64(d2)},
}
if got, want := a.Size(), 12*2; got != want {
t.Errorf("size = %d, want %d", got, want)
}
want := []uint{1, 2, 2, 3, 4, 4, 5, 6, 6, 4, 8, 0, 7, 4, 3, 13, 15, 16, 9, 14, 10, 12, 0, 2}
for idx, want := range want {
if got := a.Get(uint(idx)); got != want {
t.Errorf("v[%d] = %d, want %d", idx, got, want)
}
}
}

View File

@ -8,6 +8,8 @@ import (
pk "github.com/Tnze/go-mc/net/packet"
)
const maxPaletteBits = 8
// DecodeChunkColumn decode the chunk data structure.
// If decoding went error, successful decoded data will be returned.
func DecodeChunkColumn(mask int32, data []byte) (*Chunk, error) {
@ -27,35 +29,35 @@ func DecodeChunkColumn(mask int32, data []byte) (*Chunk, error) {
return &c, nil
}
func perBits(BitsPerBlock byte) int {
func perBits(bpb byte) uint {
switch {
case BitsPerBlock <= 4:
case bpb <= 4:
return 4
case BitsPerBlock < 9:
return int(BitsPerBlock)
case bpb <= maxPaletteBits:
return uint(bpb)
default:
return block.BitsPerBlock
return uint(block.BitsPerBlock)
}
}
func readSection(data pk.DecodeReader) (s Section, err error) {
var BlockCount pk.Short
if err := BlockCount.Decode(data); err != nil {
return nil, fmt.Errorf("read block count error: %w", err)
var nonAirBlockCount pk.Short
if err := nonAirBlockCount.Decode(data); err != nil {
return nil, fmt.Errorf("block count: %w", err)
}
var bpb pk.UnsignedByte
if err := bpb.Decode(data); err != nil {
return nil, fmt.Errorf("read bits per block error: %w", err)
return nil, fmt.Errorf("bits per block: %w", err)
}
// If bpb values greater than or equal to 9, use directSection.
// Otherwise use paletteSection.
var palettes []BlockStatus
var palettesIndex map[BlockStatus]int
if bpb < 9 {
if bpb <= maxPaletteBits {
// read palettes
var length pk.VarInt
if err := length.Decode(data); err != nil {
return nil, fmt.Errorf("read palettes length error: %w", err)
return nil, fmt.Errorf("palette length: %w", err)
}
palettes = make([]BlockStatus, length)
palettesIndex = make(map[BlockStatus]int, length)
@ -86,8 +88,15 @@ func readSection(data pk.DecodeReader) (s Section, err error) {
dataArray[i] = uint64(v)
}
sec := directSection{bpb: perBits(byte(bpb)), data: dataArray}
if bpb < 9 {
width := perBits(byte(bpb))
sec := directSection{
bitArray{
width: width,
valsPerElement: valsPerBitArrayElement(width),
data: dataArray,
},
}
if bpb <= maxPaletteBits {
return &paletteSection{
palette: palettes,
palettesIndex: palettesIndex,
@ -99,46 +108,38 @@ func readSection(data pk.DecodeReader) (s Section, err error) {
}
type directSection struct {
bpb int
data []uint64
bitArray
}
func (d *directSection) GetBlock(offset int) BlockStatus {
offset *= d.bpb
padding := offset % 64
block := uint32(d.data[offset/64] >> padding)
if padding > 64-d.bpb {
l := 64 - padding
block |= uint32(d.data[offset/64+1] << l)
}
return BlockStatus(block & (1<<d.bpb - 1)) // mask
func (d *directSection) GetBlock(offset uint) BlockStatus {
return BlockStatus(d.Get(offset))
}
func (d *directSection) SetBlock(offset int, s BlockStatus) {
offset *= d.bpb
padding := offset % 64
mask := ^uint64((1<<d.bpb - 1) << padding)
d.data[offset/64] = d.data[offset/64]&mask | uint64(s)<<padding
if padding > 64-d.bpb {
l := padding - (64 - d.bpb)
const maxUint64 = 1<<64 - 1
d.data[offset/64+1] = d.data[offset/64+1]&(maxUint64<<l) | uint64(s)>>(64-padding)
}
func (d *directSection) SetBlock(offset uint, s BlockStatus) {
d.Set(offset, uint(s))
}
func (d *directSection) CanContain(s BlockStatus) bool {
return s <= (1<<d.bpb - 1)
return s <= (1<<d.width - 1)
}
func (d *directSection) clone(bpb int) *directSection {
newSection := &directSection{
bpb: bpb,
data: make([]uint64, 16*16*16*bpb/64),
func (d *directSection) clone(bpb uint) *directSection {
out := newSectionWithSize(bpb)
for offset := uint(0); offset < 16*16*16; offset++ {
out.SetBlock(offset, d.GetBlock(offset))
}
for offset := 0; offset < 16*16*16; offset++ {
newSection.SetBlock(offset, d.GetBlock(offset))
return out
}
func newSectionWithSize(bpb uint) *directSection {
valsPerElement := valsPerBitArrayElement(bpb)
return &directSection{
bitArray{
width: bpb,
valsPerElement: valsPerElement,
data: make([]uint64, 16*16*16/valsPerElement),
},
}
return newSection
}
type paletteSection struct {
@ -147,12 +148,12 @@ type paletteSection struct {
directSection
}
func (p *paletteSection) GetBlock(offset int) BlockStatus {
func (p *paletteSection) GetBlock(offset uint) BlockStatus {
v := p.directSection.GetBlock(offset)
return p.palette[v]
}
func (p *paletteSection) SetBlock(offset int, s BlockStatus) {
func (p *paletteSection) SetBlock(offset uint, s BlockStatus) {
if i, ok := p.palettesIndex[s]; ok {
p.directSection.SetBlock(offset, BlockStatus(i))
return
@ -164,7 +165,7 @@ func (p *paletteSection) SetBlock(offset int, s BlockStatus) {
// Increase the underlying directSection
// Suppose that old bpb fit len(p.palette) before it appended.
// So bpb+1 must enough for new len(p.palette).
p.directSection = *p.directSection.clone(p.bpb + 1)
p.directSection = *p.directSection.clone(p.width + 1)
}
p.directSection.SetBlock(offset, BlockStatus(i))
}

View File

@ -1,79 +0,0 @@
package world
import (
"math/rand"
"testing"
"github.com/Tnze/go-mc/data/block"
)
func newDirectSection(bpb int) Section {
return &directSection{
bpb: bpb,
data: make([]uint64, 16*16*16*bpb/64),
}
}
func TestDirectSection(t *testing.T) {
for bpb := 4; bpb <= block.BitsPerBlock; bpb++ {
testSection(newDirectSection(bpb), bpb)(t)
}
}
func TestDirectSection_clone(t *testing.T) {
s := newDirectSection(9)
dataset := randData(9)
for i := 0; i < 16*16*16; i++ {
s.SetBlock(i, dataset[i])
}
s = s.(*directSection).clone(block.BitsPerBlock)
for i := 0; i < 16*16*16; i++ {
if s := s.GetBlock(i); dataset[i] != s {
t.Fatalf("direct section error: want: %v, get %v", dataset[i], s)
}
}
}
func TestPaletteSection(t *testing.T) {
t.Run("Correctness", testSection(&paletteSection{
palettesIndex: make(map[BlockStatus]int),
directSection: *(newDirectSection(7).(*directSection)),
}, 7))
t.Run("AutomaticExpansion", testSection(&paletteSection{
palettesIndex: make(map[BlockStatus]int),
directSection: *(newDirectSection(4).(*directSection)),
}, 9))
}
func testSection(s Section, bpb int) func(t *testing.T) {
return func(t *testing.T) {
for _, dataset := range [][16 * 16 * 16]BlockStatus{secData(bpb), randData(bpb)} {
for i := 0; i < 16*16*16; i++ {
s.SetBlock(i, dataset[i])
}
for i := 0; i < 16*16*16; i++ {
if v := s.GetBlock(i); dataset[i] != v {
t.Fatalf("direct section error: want: %v, get %v", dataset[i], v)
}
}
}
}
}
func secData(bpb int) (data [16 * 16 * 16]BlockStatus) {
mask := 1<<bpb - 1
var v int
for i := range data {
data[i] = BlockStatus(v)
v = (v + 1) & mask
}
return
}
func randData(bpb int) (data [16 * 16 * 16]BlockStatus) {
data = secData(bpb)
rand.Shuffle(len(data), func(i, j int) {
data[i], data[j] = data[j], data[i]
})
return
}

View File

@ -2,6 +2,8 @@ package world
import (
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/data/block"
pk "github.com/Tnze/go-mc/net/packet"
)
// World record all of the things in the world where player at
@ -18,16 +20,16 @@ type Chunk struct {
// Section store a 16*16*16 cube blocks
type Section interface {
// GetBlock return block status, offset can be calculate by SectionOffset.
GetBlock(offset int) BlockStatus
GetBlock(offset uint) BlockStatus
// SetBlock is the reverse operation of GetBlock.
SetBlock(offset int, s BlockStatus)
SetBlock(offset uint, s BlockStatus)
}
func SectionOffset(x, y, z int) (offset int) {
func SectionIdx(x, y, z int) uint {
// According to wiki.vg: Data Array is given for each block with increasing x coordinates,
// within rows of increasing z coordinates, within layers of increasing y coordinates.
// So offset equals to ( x*16^0 + z*16^1 + y*16^2 )*(bits per block).
return x + z*16 + y*16*16
return uint(((y & 15) << 8) | (z << 4) | x)
}
type BlockStatus uint32
@ -59,14 +61,51 @@ func (w *World) GetBlockStatus(x, y, z int) BlockStatus {
// Use n>>4 rather then n/16. It acts wrong if n<0.
c := w.Chunks[ChunkLoc{x >> 4, z >> 4}]
if c != nil {
// (n&(16-1)) == (n<0 ? n%16+16 : n%16)
if sec := c.Sections[y>>4]; sec != nil {
return sec.GetBlock(SectionOffset(x&15, y&15, z&15))
return sec.GetBlock(SectionIdx(x&15, y&15, z&15))
}
}
return 0
}
func (w *World) UnaryBlockUpdate(pos pk.Position, bStateID BlockStatus) bool {
c := w.Chunks[ChunkLoc{X: pos.X >> 4, Z: pos.Z >> 4}]
if c == nil {
return false
}
sIdx, bIdx := pos.Y>>4, SectionIdx(pos.X&15, pos.Y&15, pos.Z&15)
if sec := c.Sections[sIdx]; sec == nil {
sec = newSectionWithSize(uint(block.BitsPerBlock))
sec.SetBlock(bIdx, bStateID)
c.Sections[sIdx] = sec
} else {
sec.SetBlock(bIdx, bStateID)
}
return true
}
func (w *World) MultiBlockUpdate(loc ChunkLoc, sectionY int, blocks []pk.VarLong) bool {
c := w.Chunks[loc]
if c == nil {
return false // not loaded
}
sec := c.Sections[sectionY]
if sec == nil {
sec = newSectionWithSize(uint(block.BitsPerBlock))
c.Sections[sectionY] = sec
}
for _, b := range blocks {
bStateID := b >> 12
x, z, y := (b>>8)&0xf, (b>>4)&0xf, b&0xf
sec.SetBlock(SectionIdx(int(x&15), int(y&15), int(z&15)), BlockStatus(bStateID))
}
return true
}
// func (b Block) String() string {
// return blockNameByID[b.id]
// }