package mcdb import ( "encoding/binary" "fmt" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/chunk" "github.com/df-mc/goleveldb/leveldb/iterator" ) // ColumnIterator iterates over a DB's position/column pairs in key order. // // When an error is encountered, any call to Next will return false and will // yield no position/chunk pairs. The error can be queried by calling the Error // method. Calling Release is still necessary. // // An iterator must be released after use, but it is not necessary to read // an iterator until exhaustion. // Also, an iterator is not necessarily safe for concurrent use, but it is // safe to use multiple iterators concurrently, with each in a dedicated // goroutine. type ColumnIterator struct { dbIter iterator.Iterator db *DB r *IteratorRange err error current *chunk.Column pos world.ChunkPos dim world.Dimension seen map[dbKey]struct{} } func newColumnIterator(db *DB, r *IteratorRange) *ColumnIterator { return &ColumnIterator{ db: db, dbIter: db.ldb.NewIterator(nil, nil), seen: make(map[dbKey]struct{}), r: r, } } // Next moves the iterator to the next key/value pair. // It returns false if the iterator is exhausted. func (iter *ColumnIterator) Next() bool { if iter.err != nil || !iter.dbIter.Next() { iter.current = nil iter.dim = nil return false } k := iter.dbIter.Key() kLen := len(k) if (kLen != 9 && kLen != 13) || (k[kLen-1] != keyVersion && k[kLen-1] != keyVersionOld) { return iter.Next() } iter.dim = world.Dimension(world.Overworld) if kLen > 9 { var ok bool id := int(binary.LittleEndian.Uint32(k[8:12])) if iter.dim, ok = world.DimensionByID(id); !ok { iter.err = fmt.Errorf("unknown dimension id %v", id) return false } } iter.pos = world.ChunkPos{ int32(binary.LittleEndian.Uint32(k[:4])), int32(binary.LittleEndian.Uint32(k[4:8])), } if !iter.r.within(iter.pos, iter.dim) { return iter.Next() } key := dbKey{dim: iter.dim, pos: iter.pos} if _, ok := iter.seen[key]; ok { // Already encountered this chunk. This might happen if there are // multiple version keys. return iter.Next() } iter.current, iter.err = iter.db.LoadColumn(iter.pos, iter.dim) if iter.err != nil { iter.err = fmt.Errorf("load chunk %v: %w", iter.pos, iter.err) return false } iter.seen[key] = struct{}{} return true } // Column returns the value of the current position/column pair, or nil if none. func (iter *ColumnIterator) Column() *chunk.Column { return iter.current } // Position returns the position of the current position/column pair. func (iter *ColumnIterator) Position() world.ChunkPos { return iter.pos } // Dimension returns the dimension of the current position/column pair, or nil // if none. func (iter *ColumnIterator) Dimension() world.Dimension { return iter.dim } // Release releases associated resources. Release should always success // and can be called multiple times without causing error. func (iter *ColumnIterator) Release() { iter.dbIter.Release() } // Error returns any accumulated error. Exhausting all the key/value pairs // is not considered to be an error. func (iter *ColumnIterator) Error() error { return iter.err } // IteratorRange is a range used to limit what columns are accumulated by a // ColumnIterator. type IteratorRange struct { // Min and Max limit what chunk positions are returned by a ColumnIterator. // A zero value for both Min and Max causes all positions to be within the // range. Min, Max world.ChunkPos // Dimension specifies what world.Dimension chunks should be accumulated // from. If nil, all dimensions will be read from. Dimension world.Dimension } // within checks if a position and dimension is within the IteratorRange. func (r *IteratorRange) within(pos world.ChunkPos, dim world.Dimension) bool { if dim != r.Dimension && r.Dimension != nil { return false } return ((r.Min == world.ChunkPos{}) && (r.Max == world.ChunkPos{})) || pos[0] >= r.Min[0] && pos[0] < r.Max[0] && pos[1] >= r.Min[1] && pos[1] < r.Max[1] }