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,72 @@
|
||||
package mcdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/mcdb/leveldat"
|
||||
"github.com/df-mc/goleveldb/leveldb"
|
||||
"github.com/df-mc/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
// Config holds the optional parameters of a DB.
|
||||
type Config struct {
|
||||
// Log is the Logger that will be used to log errors and debug messages to.
|
||||
// If set to nil, Log is set to slog.Default().
|
||||
Log *slog.Logger
|
||||
// LDBOptions holds LevelDB specific default options, such as the block size
|
||||
// or compression used in the database.
|
||||
LDBOptions *opt.Options
|
||||
|
||||
// Blocks is the BlockRegistry used for chunk decoding/encoding. If nil, world.DefaultBlockRegistry is used.
|
||||
// When using a non-default registry, pass the same registry used by the World.
|
||||
Blocks world.BlockRegistry
|
||||
}
|
||||
|
||||
// Open creates a new DB reading and writing from/to files under the path
|
||||
// passed. If a world is present at the path, Open will parse its data and
|
||||
// initialise the world with it. If the data cannot be parsed, an error is
|
||||
// returned.
|
||||
func (conf Config) Open(dir string) (*DB, error) {
|
||||
if conf.Log == nil {
|
||||
conf.Log = slog.Default()
|
||||
}
|
||||
conf.Log = conf.Log.With("provider", "mcdb")
|
||||
if conf.LDBOptions == nil {
|
||||
conf.LDBOptions = new(opt.Options)
|
||||
}
|
||||
if conf.LDBOptions.BlockSize == 0 {
|
||||
conf.LDBOptions.BlockSize = 16 * opt.KiB
|
||||
}
|
||||
|
||||
_ = os.MkdirAll(filepath.Join(dir, "db"), 0777)
|
||||
|
||||
db := &DB{conf: conf, dir: dir, ldat: &leveldat.Data{}}
|
||||
db.SetBlockRegistry(conf.Blocks)
|
||||
if _, err := os.Stat(filepath.Join(dir, "level.dat")); os.IsNotExist(err) {
|
||||
// A level.dat was not currently present for the world.
|
||||
db.ldat.FillDefault()
|
||||
} else {
|
||||
ldat, err := leveldat.ReadFile(filepath.Join(dir, "level.dat"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open db: read level.dat: %w", err)
|
||||
}
|
||||
ver := ldat.Ver()
|
||||
if ver != leveldat.Version && ver >= 10 {
|
||||
return nil, fmt.Errorf("open db: level.dat version %v is unsupported", ver)
|
||||
}
|
||||
if err = ldat.Unmarshal(db.ldat); err != nil {
|
||||
return nil, fmt.Errorf("open db: unmarshal level.dat: %w", err)
|
||||
}
|
||||
}
|
||||
db.set = db.ldat.Settings()
|
||||
ldb, err := leveldb.OpenFile(filepath.Join(dir, "db"), conf.LDBOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open db: leveldb: %w", err)
|
||||
}
|
||||
db.ldb = ldb
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
package mcdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/chunk"
|
||||
"github.com/df-mc/dragonfly/server/world/mcdb/leveldat"
|
||||
"github.com/df-mc/goleveldb/leveldb"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sandertv/gophertunnel/minecraft/nbt"
|
||||
)
|
||||
|
||||
// DB implements a world provider for the Minecraft world format, which
|
||||
// is based on a leveldb database.
|
||||
type DB struct {
|
||||
conf Config
|
||||
ldb *leveldb.DB
|
||||
dir string
|
||||
ldat *leveldat.Data
|
||||
set *world.Settings
|
||||
}
|
||||
|
||||
// Open creates a new provider reading and writing from/to files under the path
|
||||
// passed using default options. If a world is present at the path, Open will
|
||||
// parse its data and initialise the world with it. If the data cannot be
|
||||
// parsed, an error is returned.
|
||||
func Open(dir string) (*DB, error) {
|
||||
var conf Config
|
||||
return conf.Open(dir)
|
||||
}
|
||||
|
||||
// SetBlockRegistry updates the block registry used for chunk and scheduled update encoding/decoding.
|
||||
// It should be called before any columns are loaded or stored.
|
||||
func (db *DB) SetBlockRegistry(br world.BlockRegistry) {
|
||||
if br == nil {
|
||||
br = world.DefaultBlockRegistry
|
||||
}
|
||||
br.Finalize()
|
||||
db.conf.Blocks = br
|
||||
}
|
||||
|
||||
// Settings returns the world.Settings of the world loaded by the DB.
|
||||
func (db *DB) Settings() *world.Settings {
|
||||
return db.set
|
||||
}
|
||||
|
||||
// SaveSettings saves the world.Settings passed to the level.dat.
|
||||
func (db *DB) SaveSettings(s *world.Settings) {
|
||||
db.ldat.PutSettings(s)
|
||||
}
|
||||
|
||||
// playerData holds the fields that indicate where player data is stored for a player with a specific UUID.
|
||||
type playerData struct {
|
||||
UUID string `nbt:"MsaId"`
|
||||
ServerID string `nbt:"ServerId"`
|
||||
SelfSignedID string `nbt:"SelfSignedId"`
|
||||
}
|
||||
|
||||
// LoadPlayerSpawnPosition loads the players spawn position stored in the level.dat from their UUID.
|
||||
func (db *DB) LoadPlayerSpawnPosition(id uuid.UUID) (pos cube.Pos, exists bool, err error) {
|
||||
serverData, _, exists, err := db.loadPlayerData(id)
|
||||
if !exists || err != nil {
|
||||
return cube.Pos{}, exists, err
|
||||
}
|
||||
x, y, z := serverData["SpawnX"], serverData["SpawnY"], serverData["SpawnZ"]
|
||||
if x == nil || y == nil || z == nil {
|
||||
return cube.Pos{}, true, fmt.Errorf("error reading spawn fields from server data for player %v", id)
|
||||
}
|
||||
return cube.Pos{int(x.(int32)), int(y.(int32)), int(z.(int32))}, true, nil
|
||||
}
|
||||
|
||||
// loadPlayerData loads the data stored in a LevelDB database for a specific UUID.
|
||||
func (db *DB) loadPlayerData(id uuid.UUID) (serverData map[string]interface{}, key string, exists bool, err error) {
|
||||
data, err := db.ldb.Get([]byte("player_"+id.String()), nil)
|
||||
if errors.Is(err, leveldb.ErrNotFound) {
|
||||
return nil, "", false, nil
|
||||
} else if err != nil {
|
||||
return nil, "", true, fmt.Errorf("error reading player data for uuid %v: %w", id, err)
|
||||
}
|
||||
|
||||
var d playerData
|
||||
if err := nbt.UnmarshalEncoding(data, &d, nbt.LittleEndian); err != nil {
|
||||
return nil, "", true, fmt.Errorf("error decoding player data for uuid %v: %w", id, err)
|
||||
}
|
||||
if d.UUID != id.String() || d.ServerID == "" {
|
||||
return nil, d.ServerID, true, fmt.Errorf("invalid player data for uuid %v: %v", id, d)
|
||||
}
|
||||
serverDB, err := db.ldb.Get([]byte(d.ServerID), nil)
|
||||
if err != nil {
|
||||
return nil, d.ServerID, true, fmt.Errorf("error reading server data for player %v (%v): %w", id, d.ServerID, err)
|
||||
}
|
||||
|
||||
if err := nbt.UnmarshalEncoding(serverDB, &serverData, nbt.LittleEndian); err != nil {
|
||||
return nil, d.ServerID, true, fmt.Errorf("error decoding server data for player %v", id)
|
||||
}
|
||||
return serverData, d.ServerID, true, nil
|
||||
}
|
||||
|
||||
// SavePlayerSpawnPosition saves the player spawn position passed to the levelDB database.
|
||||
func (db *DB) SavePlayerSpawnPosition(id uuid.UUID, pos cube.Pos) error {
|
||||
_, err := db.ldb.Get([]byte("player_"+id.String()), nil)
|
||||
d := make(map[string]interface{})
|
||||
k := "player_server_" + id.String()
|
||||
|
||||
if errors.Is(err, leveldb.ErrNotFound) {
|
||||
data, err := nbt.MarshalEncoding(playerData{UUID: id.String(), ServerID: k}, nbt.LittleEndian)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := db.ldb.Put([]byte("player_"+id.String()), data, nil); err != nil {
|
||||
return fmt.Errorf("write player data (uuid=%v): %w", id, err)
|
||||
}
|
||||
} else if d, k, _, err = db.loadPlayerData(id); err != nil {
|
||||
return err
|
||||
}
|
||||
d["SpawnX"], d["SpawnY"], d["SpawnZ"] = int32(pos.X()), int32(pos.Y()), int32(pos.Z())
|
||||
|
||||
data, err := nbt.MarshalEncoding(d, nbt.LittleEndian)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = db.ldb.Put([]byte(k), data, nil); err != nil {
|
||||
return fmt.Errorf("write server data for player %v: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadColumn reads a world.Column from the DB at a position and dimension in
|
||||
// the DB. If no column at that position exists, errors.Is(err,
|
||||
// leveldb.ErrNotFound) equals true.
|
||||
func (db *DB) LoadColumn(pos world.ChunkPos, dim world.Dimension) (*chunk.Column, error) {
|
||||
k := dbKey{pos: pos, dim: dim}
|
||||
col, err := db.column(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load column %v (%v): %w", pos, dim, err)
|
||||
}
|
||||
return col, nil
|
||||
}
|
||||
|
||||
const chunkVersion = 42
|
||||
|
||||
func (db *DB) column(k dbKey) (*chunk.Column, error) {
|
||||
var cdata chunk.SerialisedData
|
||||
col := new(chunk.Column)
|
||||
|
||||
ver, err := db.version(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read version: %w", err)
|
||||
}
|
||||
if ver != chunkVersion {
|
||||
db.conf.Log.Debug("column: unsupported chunk version, trying to load anyway", "X", k.pos[0], "Z", k.pos[1], "dimension", fmt.Sprint(k.dim), "ver", ver)
|
||||
}
|
||||
cdata.Biomes, err = db.biomes(k)
|
||||
if err != nil && !errors.Is(err, leveldb.ErrNotFound) {
|
||||
// Some chunks still use 2D chunk data and might not have this field, in
|
||||
// which case we can just move on.
|
||||
return nil, fmt.Errorf("read biomes: %w", err)
|
||||
}
|
||||
cdata.SubChunks, err = db.subChunks(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read sub chunks: %w", err)
|
||||
}
|
||||
col.Chunk, err = chunk.DiskDecode(db.conf.Blocks, cdata, k.dim.Range())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode chunk data: %w", err)
|
||||
}
|
||||
col.Entities, err = db.entities(k)
|
||||
if err != nil && !errors.Is(err, leveldb.ErrNotFound) {
|
||||
// Not all chunks need to have entities, so an ErrNotFound is fine here.
|
||||
return nil, fmt.Errorf("read entities: %w", err)
|
||||
}
|
||||
col.BlockEntities, err = db.blockEntities(k)
|
||||
if err != nil && !errors.Is(err, leveldb.ErrNotFound) {
|
||||
// Same as with entities, an ErrNotFound is fine here.
|
||||
return nil, fmt.Errorf("read block entities: %w", err)
|
||||
}
|
||||
col.ScheduledBlocks, col.Tick, err = db.scheduledUpdates(k)
|
||||
if err != nil && !errors.Is(err, leveldb.ErrNotFound) {
|
||||
return nil, fmt.Errorf("read scheduled updates: %w", err)
|
||||
}
|
||||
return col, nil
|
||||
}
|
||||
|
||||
func (db *DB) version(k dbKey) (byte, error) {
|
||||
p, err := db.ldb.Get(k.Sum(keyVersion), nil)
|
||||
if errors.Is(err, leveldb.ErrNotFound) {
|
||||
// Although the version at `keyVersion` may not be found, there is
|
||||
// another `keyVersionOld` where the version may be found.
|
||||
if p, err = db.ldb.Get(k.Sum(keyVersionOld), nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n := len(p); n != 1 {
|
||||
return 0, fmt.Errorf("expected 1 version byte, got %v", n)
|
||||
}
|
||||
return p[0], nil
|
||||
}
|
||||
|
||||
func (db *DB) biomes(k dbKey) ([]byte, error) {
|
||||
biomes, err := db.ldb.Get(k.Sum(key3DData), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The first 512 bytes is a heightmap (16*16 int16s), the biomes follow. We
|
||||
// calculate a heightmap on startup so the heightmap can be discarded.
|
||||
if n := len(biomes); n <= 512 {
|
||||
return nil, fmt.Errorf("expected at least 513 bytes for 3D data, got %v", n)
|
||||
}
|
||||
return biomes[512:], nil
|
||||
}
|
||||
|
||||
func (db *DB) subChunks(k dbKey) ([][]byte, error) {
|
||||
r := k.dim.Range()
|
||||
sub := make([][]byte, (r.Height()>>4)+1)
|
||||
|
||||
var err error
|
||||
for i := range sub {
|
||||
y := uint8(i + (r[0] >> 4))
|
||||
sub[i], err = db.ldb.Get(k.Sum(keySubChunkData, y), nil)
|
||||
if errors.Is(err, leveldb.ErrNotFound) {
|
||||
// No sub chunk present at this Y level. We skip this one and move
|
||||
// to the next, which might still be present.
|
||||
continue
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("sub chunk %v: %w", int8(i), err)
|
||||
}
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (db *DB) entities(k dbKey) ([]chunk.Entity, error) {
|
||||
// https://learn.microsoft.com/en-us/minecraft/creator/documents/actorstorage
|
||||
ids, err := db.ldb.Get(append([]byte(keyEntityIdentifiers), index(k.pos, k.dim)...), nil)
|
||||
if err != nil {
|
||||
// Key not found, try old method of loading entities.
|
||||
return db.entitiesOld(k)
|
||||
}
|
||||
entities := make([]chunk.Entity, 0, len(ids)/8)
|
||||
for i := 0; i < len(ids); i += 8 {
|
||||
id := int64(binary.LittleEndian.Uint64(ids[i : i+8]))
|
||||
data, err := db.ldb.Get(entityIndex(id), nil)
|
||||
if err != nil {
|
||||
db.conf.Log.Error("read entity: "+err.Error(), "ID", id)
|
||||
return nil, err
|
||||
}
|
||||
ent := chunk.Entity{ID: id, Data: make(map[string]any)}
|
||||
if err = nbt.UnmarshalEncoding(data, &ent.Data, nbt.LittleEndian); err != nil {
|
||||
db.conf.Log.Error("decode entity nbt: "+err.Error(), "ID", id)
|
||||
}
|
||||
entities = append(entities, ent)
|
||||
}
|
||||
return entities, nil
|
||||
}
|
||||
|
||||
func (db *DB) entitiesOld(k dbKey) ([]chunk.Entity, error) {
|
||||
data, err := db.ldb.Get(k.Sum(keyEntitiesOld), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var entities []chunk.Entity
|
||||
|
||||
buf := bytes.NewBuffer(data)
|
||||
dec, ok := nbt.NewDecoderWithEncoding(buf, nbt.LittleEndian), false
|
||||
|
||||
for buf.Len() != 0 {
|
||||
ent := chunk.Entity{Data: make(map[string]any)}
|
||||
if err := dec.Decode(&ent.Data); err != nil {
|
||||
return nil, fmt.Errorf("decode entity nbt: %w", err)
|
||||
}
|
||||
ent.ID, ok = ent.Data["UniqueID"].(int64)
|
||||
if !ok {
|
||||
db.conf.Log.Error("missing unique ID field, generating random", "data", fmt.Sprint(ent.Data))
|
||||
ent.ID = rand.Int64()
|
||||
}
|
||||
entities = append(entities, ent)
|
||||
}
|
||||
return entities, nil
|
||||
}
|
||||
|
||||
func (db *DB) blockEntities(k dbKey) ([]chunk.BlockEntity, error) {
|
||||
var blockEntities []chunk.BlockEntity
|
||||
|
||||
data, err := db.ldb.Get(k.Sum(keyBlockEntities), nil)
|
||||
if err != nil {
|
||||
return blockEntities, err
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(data)
|
||||
dec := nbt.NewDecoderWithEncoding(buf, nbt.LittleEndian)
|
||||
|
||||
for buf.Len() != 0 {
|
||||
be := chunk.BlockEntity{Data: make(map[string]any)}
|
||||
if err := dec.Decode(&be.Data); err != nil {
|
||||
return blockEntities, fmt.Errorf("decode nbt: %w", err)
|
||||
}
|
||||
be.Pos = blockPosFromNBT(be.Data)
|
||||
blockEntities = append(blockEntities, be)
|
||||
}
|
||||
return blockEntities, nil
|
||||
}
|
||||
|
||||
func (db *DB) scheduledUpdates(k dbKey) ([]chunk.ScheduledBlockUpdate, int64, error) {
|
||||
data, err := db.ldb.Get(k.Sum(keyPendingScheduledTicks), nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var m scheduledUpdates
|
||||
if err := nbt.UnmarshalEncoding(data, &m, nbt.LittleEndian); err != nil {
|
||||
return nil, 0, fmt.Errorf("read nbt: %s", err.Error())
|
||||
}
|
||||
updates := make([]chunk.ScheduledBlockUpdate, len(m.TickList))
|
||||
for i, tick := range m.TickList {
|
||||
t, _ := tick["time"].(int64)
|
||||
bl, _ := tick["blockState"].(map[string]any)
|
||||
bpe := chunk.BlockPaletteEncoding{Blocks: db.conf.Blocks}
|
||||
block, err := bpe.DecodeBlockState(bl)
|
||||
if err != nil {
|
||||
db.conf.Log.Error("read scheduled updates: decode block state: " + err.Error())
|
||||
continue
|
||||
}
|
||||
updates[i] = chunk.ScheduledBlockUpdate{Pos: blockPosFromNBT(tick), Block: block, Tick: t}
|
||||
}
|
||||
return updates, int64(m.CurrentTick), nil
|
||||
}
|
||||
|
||||
// StoreColumn stores a world.Column at a position and dimension in the DB. An
|
||||
// error is returned if storing was unsuccessful.
|
||||
func (db *DB) StoreColumn(pos world.ChunkPos, dim world.Dimension, col *chunk.Column) error {
|
||||
k := dbKey{pos: pos, dim: dim}
|
||||
if err := db.storeColumn(k, col); err != nil {
|
||||
return fmt.Errorf("store column %v (%v): %w", pos, dim, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) storeColumn(k dbKey, col *chunk.Column) error {
|
||||
data := chunk.Encode(col.Chunk, chunk.DiskEncoding)
|
||||
n := 7 + len(data.SubChunks) + len(col.Entities)
|
||||
batch := leveldb.MakeBatch(n)
|
||||
|
||||
db.storeVersion(batch, k, chunkVersion)
|
||||
db.storeBiomes(batch, k, data.Biomes)
|
||||
db.storeSubChunks(batch, k, data.SubChunks, col.Chunk.Range())
|
||||
db.storeFinalisation(batch, k, finalisationPopulated)
|
||||
db.storeEntities(batch, k, col.Entities)
|
||||
db.storeBlockEntities(batch, k, col.BlockEntities)
|
||||
db.storeScheduledUpdates(batch, k, col.Tick, col.ScheduledBlocks)
|
||||
|
||||
return db.ldb.Write(batch, nil)
|
||||
}
|
||||
|
||||
func (db *DB) storeVersion(batch *leveldb.Batch, k dbKey, ver uint8) {
|
||||
batch.Put(k.Sum(keyVersion), []byte{ver})
|
||||
}
|
||||
|
||||
var emptyHeightmap = make([]byte, 512)
|
||||
|
||||
func (db *DB) storeBiomes(batch *leveldb.Batch, k dbKey, biomes []byte) {
|
||||
batch.Put(k.Sum(key3DData), append(emptyHeightmap, biomes...))
|
||||
}
|
||||
|
||||
func (db *DB) storeSubChunks(batch *leveldb.Batch, k dbKey, subChunks [][]byte, r cube.Range) {
|
||||
for i, sub := range subChunks {
|
||||
batch.Put(k.Sum(keySubChunkData, byte(i+(r[0]>>4))), sub)
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DB) storeFinalisation(batch *leveldb.Batch, k dbKey, finalisation uint32) {
|
||||
p := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(p, finalisation)
|
||||
batch.Put(k.Sum(keyFinalisation), p)
|
||||
}
|
||||
|
||||
func (db *DB) storeEntities(batch *leveldb.Batch, k dbKey, entities []chunk.Entity) {
|
||||
idsKey := append([]byte(keyEntityIdentifiers), index(k.pos, k.dim)...)
|
||||
|
||||
// load the ids of the previous entities
|
||||
var previousIDs []int64
|
||||
digpPrev, err := db.ldb.Get(idsKey, nil)
|
||||
if err != nil && !errors.Is(err, leveldb.ErrNotFound) {
|
||||
db.conf.Log.Error("store entities: read chunk entity IDs: " + err.Error())
|
||||
}
|
||||
if err == nil {
|
||||
for i := 0; i < len(digpPrev); i += 8 {
|
||||
previousIDs = append(previousIDs, int64(binary.LittleEndian.Uint64(digpPrev[i:])))
|
||||
}
|
||||
}
|
||||
|
||||
newIDs := make([]int64, 0, len(entities))
|
||||
for _, e := range entities {
|
||||
e.Data["UniqueID"] = e.ID
|
||||
b, err := nbt.MarshalEncoding(e.Data, nbt.LittleEndian)
|
||||
if err != nil {
|
||||
db.conf.Log.Error("store entities: encode NBT: " + err.Error())
|
||||
continue
|
||||
}
|
||||
batch.Put(entityIndex(e.ID), b)
|
||||
newIDs = append(newIDs, e.ID)
|
||||
}
|
||||
|
||||
// Remove entities that are no longer referenced.
|
||||
for _, uniqueID := range previousIDs {
|
||||
if !slices.Contains(newIDs, uniqueID) {
|
||||
batch.Delete(entityIndex(uniqueID))
|
||||
}
|
||||
}
|
||||
if len(entities) == 0 {
|
||||
batch.Delete(idsKey)
|
||||
} else {
|
||||
// Save the index of entities in the chunk.
|
||||
ids := make([]byte, 0, 8*len(newIDs))
|
||||
for _, uniqueID := range newIDs {
|
||||
ids = binary.LittleEndian.AppendUint64(ids, uint64(uniqueID))
|
||||
}
|
||||
batch.Put(idsKey, ids)
|
||||
}
|
||||
|
||||
// Remove old entity data for this chunk.
|
||||
batch.Delete(k.Sum(keyEntitiesOld))
|
||||
}
|
||||
|
||||
func entityIndex(id int64) []byte {
|
||||
return binary.LittleEndian.AppendUint64([]byte(keyEntity), uint64(id))
|
||||
}
|
||||
|
||||
func (db *DB) storeBlockEntities(batch *leveldb.Batch, k dbKey, blockEntities []chunk.BlockEntity) {
|
||||
if len(blockEntities) == 0 {
|
||||
batch.Delete(k.Sum(keyBlockEntities))
|
||||
return
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
enc := nbt.NewEncoderWithEncoding(buf, nbt.LittleEndian)
|
||||
for _, b := range blockEntities {
|
||||
b.Data["x"], b.Data["y"], b.Data["z"] = int32(b.Pos[0]), int32(b.Pos[1]), int32(b.Pos[2])
|
||||
if err := enc.Encode(b.Data); err != nil {
|
||||
db.conf.Log.Error("store block entities: encode nbt: " + err.Error())
|
||||
}
|
||||
}
|
||||
batch.Put(k.Sum(keyBlockEntities), buf.Bytes())
|
||||
}
|
||||
|
||||
func (db *DB) storeScheduledUpdates(batch *leveldb.Batch, k dbKey, tick int64, updates []chunk.ScheduledBlockUpdate) {
|
||||
if len(updates) == 0 {
|
||||
batch.Delete(k.Sum(keyPendingScheduledTicks))
|
||||
return
|
||||
}
|
||||
list := make([]map[string]any, len(updates))
|
||||
bpe := chunk.BlockPaletteEncoding{Blocks: db.conf.Blocks}
|
||||
for i, update := range updates {
|
||||
list[i] = map[string]any{
|
||||
"x": int32(update.Pos[0]), "y": int32(update.Pos[1]), "z": int32(update.Pos[2]),
|
||||
"time": update.Tick, "blockState": bpe.EncodeBlockState(update.Block),
|
||||
}
|
||||
}
|
||||
b, err := nbt.MarshalEncoding(scheduledUpdates{CurrentTick: int32(tick), TickList: list}, nbt.LittleEndian)
|
||||
if err != nil {
|
||||
db.conf.Log.Error("store scheduled updates: encode nbt: " + err.Error())
|
||||
return
|
||||
}
|
||||
batch.Put(k.Sum(keyPendingScheduledTicks), b)
|
||||
}
|
||||
|
||||
type scheduledUpdates struct {
|
||||
CurrentTick int32 `nbt:"currentTick"`
|
||||
TickList []map[string]any `nbt:"tickList"`
|
||||
}
|
||||
|
||||
// NewColumnIterator returns a ColumnIterator that may be used to iterate over all
|
||||
// position/chunk pairs in a database.
|
||||
// An IteratorRange r may be passed to specify limits in terms of what chunks
|
||||
// should be read. r may be set to nil to read all chunks from the DB.
|
||||
func (db *DB) NewColumnIterator(r *IteratorRange) *ColumnIterator {
|
||||
if r == nil {
|
||||
r = &IteratorRange{}
|
||||
}
|
||||
return newColumnIterator(db, r)
|
||||
}
|
||||
|
||||
// Close closes the provider, saving any file that might need to be saved, such as the level.dat.
|
||||
func (db *DB) Close() error {
|
||||
db.ldat.LastPlayed = time.Now().Unix()
|
||||
|
||||
var ldat leveldat.LevelDat
|
||||
if err := ldat.Marshal(*db.ldat); err != nil {
|
||||
return fmt.Errorf("close: %w", err)
|
||||
}
|
||||
if err := ldat.WriteFile(filepath.Join(db.dir, "level.dat")); err != nil {
|
||||
return fmt.Errorf("close: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(db.dir, "levelname.txt"), []byte(db.ldat.LevelName), 0644); err != nil {
|
||||
return fmt.Errorf("close: write levelname.txt: %w", err)
|
||||
}
|
||||
return db.ldb.Close()
|
||||
}
|
||||
|
||||
// dbKey holds a position and dimension.
|
||||
type dbKey struct {
|
||||
pos world.ChunkPos
|
||||
dim world.Dimension
|
||||
}
|
||||
|
||||
// Sum converts k to its []byte representation and appends p.
|
||||
func (k dbKey) Sum(p ...byte) []byte {
|
||||
return append(index(k.pos, k.dim), p...)
|
||||
}
|
||||
|
||||
// index returns a byte buffer holding the written index of the chunk position passed. If the dimension passed
|
||||
// is not world.Overworld, the length of the index returned is 12. It is 8 otherwise.
|
||||
func index(position world.ChunkPos, d world.Dimension) []byte {
|
||||
dim, _ := world.DimensionID(d)
|
||||
x, z := uint32(position[0]), uint32(position[1])
|
||||
b := make([]byte, 12)
|
||||
|
||||
binary.LittleEndian.PutUint32(b, x)
|
||||
binary.LittleEndian.PutUint32(b[4:], z)
|
||||
if dim == 0 {
|
||||
return b[:8]
|
||||
}
|
||||
binary.LittleEndian.PutUint32(b[8:], uint32(dim))
|
||||
return b
|
||||
}
|
||||
|
||||
// blockPosFromNBT returns a position from the X, Y and Z components stored in the NBT data map passed. The
|
||||
// map is assumed to have an 'x', 'y' and 'z' key.
|
||||
func blockPosFromNBT(data map[string]any) cube.Pos {
|
||||
x, _ := data["x"].(int32)
|
||||
y, _ := data["y"].(int32)
|
||||
z, _ := data["z"].(int32)
|
||||
return cube.Pos{int(x), int(y), int(z)}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
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]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package mcdb
|
||||
|
||||
//lint:file-ignore U1000 Unused unexported constants are present for future code using these.
|
||||
|
||||
// Keys on a per-sub chunk basis. These are prefixed by the chunk coordinates and subchunk ID.
|
||||
const (
|
||||
keySubChunkData = '/' // 2f
|
||||
)
|
||||
|
||||
// Keys on a per-chunk basis. These are prefixed by only the chunk coordinates.
|
||||
const (
|
||||
// keyVersion holds a single byte of data with the version of the chunk.
|
||||
keyVersion = ',' // 2c
|
||||
// keyVersionOld was replaced by keyVersion. It is still used by vanilla to check compatibility, but vanilla no
|
||||
// longer writes this tag.
|
||||
keyVersionOld = 'v' // 76
|
||||
// keyBlockEntities holds n amount of NBT compound tags appended to each other (not a TAG_List, just appended). The
|
||||
// compound tags contain the position of the block entities.
|
||||
keyBlockEntities = '1' // 31
|
||||
// keyEntitiesOld holds n amount of NBT compound tags appended to each other (not a TAG_List, just appended). The
|
||||
// compound tags contain the position of the entities.
|
||||
keyEntitiesOld = '2' // 32
|
||||
// keyPendingScheduledTicks holds an NBT structure containing all scheduled
|
||||
// ticks that were pending in the chunk.
|
||||
keyPendingScheduledTicks = '3'
|
||||
// keyFinalisation contains a single LE int32 that indicates the state of generation of the chunk. If 0, the chunk
|
||||
// needs to be ticked. If 1, the chunk needs to be populated and if 2 (which is the state generally found in world
|
||||
// saves from vanilla), the chunk is fully finalised.
|
||||
keyFinalisation = '6' // 36
|
||||
// key3DData holds 3-dimensional biomes for the entire chunk.
|
||||
key3DData = '+' // 2b
|
||||
// key2DData is no longer used in worlds with world height change. It was replaced by key3DData in newer worlds
|
||||
// which has 3-dimensional biomes.
|
||||
key2DData = '-' // 2d
|
||||
// keyChecksum holds a list of checksums of some sort. It's not clear of what data this checksum is composed or what
|
||||
// these checksums are used for.
|
||||
keyChecksums = ';' // 3b
|
||||
|
||||
keyEntityIdentifiers = "digp"
|
||||
|
||||
keyEntity = "actorprefix"
|
||||
)
|
||||
|
||||
// Keys on a per-world basis. These are found only once in a leveldb world save.
|
||||
const (
|
||||
keyAutonomousEntities = "AutonomousEntities"
|
||||
keyOverworld = "Overworld"
|
||||
keyMobEvents = "mobevents"
|
||||
keyBiomeData = "BiomeData"
|
||||
keyScoreboard = "scoreboard"
|
||||
keyLocalPlayer = "~local_player"
|
||||
)
|
||||
|
||||
const (
|
||||
finalisationGenerated = iota + 1
|
||||
finalisationPopulated
|
||||
)
|
||||
@@ -0,0 +1,275 @@
|
||||
package leveldat
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
)
|
||||
|
||||
// Data holds a collection of data that specify a range of Settings of the
|
||||
// world. These Settings usually alter the way that players interact with the
|
||||
// world. The data held here is usually saved in a level.dat file of the world.
|
||||
// Data may be used in LevelDat.Unmarshal to collect the data of the level.dat.
|
||||
type Data struct {
|
||||
BaseGameVersion string `nbt:"baseGameVersion"`
|
||||
BiomeOverride string
|
||||
ConfirmedPlatformLockedContent bool
|
||||
CentreMapsToOrigin bool `nbt:"CenterMapsToOrigin"`
|
||||
CheatsEnabled bool `nbt:"cheatsEnabled"`
|
||||
DaylightCycle int32 `nbt:"daylightCycle"`
|
||||
Difficulty int32
|
||||
EduOffer int32 `nbt:"eduOffer"`
|
||||
FlatWorldLayers string
|
||||
ForceGameType bool
|
||||
GameType int32
|
||||
Generator int32
|
||||
InventoryVersion string
|
||||
LANBroadcast bool
|
||||
LANBroadcastIntent bool
|
||||
LastPlayed int64
|
||||
LevelName string
|
||||
LimitedWorldOriginX int32
|
||||
LimitedWorldOriginY int32
|
||||
LimitedWorldOriginZ int32
|
||||
LimitedWorldDepth int32 `nbt:"limitedWorldDepth"`
|
||||
LimitedWorldWidth int32 `nbt:"limitedWorldWidth"`
|
||||
LocatorBar bool `nbt:"locatorbar"`
|
||||
MinimumCompatibleClientVersion []int32
|
||||
MultiPlayerGame bool `nbt:"MultiplayerGame"`
|
||||
MultiPlayerGameIntent bool `nbt:"MultiplayerGameIntent"`
|
||||
NetherScale int32
|
||||
NetworkVersion int32
|
||||
Platform int32
|
||||
PlatformBroadcastIntent int32
|
||||
RandomSeed int64
|
||||
ShowTags bool `nbt:"showtags"`
|
||||
SingleUseWorld bool `nbt:"isSingleUseWorld"`
|
||||
SpawnX, SpawnY, SpawnZ int32
|
||||
SpawnV1Villagers bool
|
||||
StorageVersion int32
|
||||
Time int64
|
||||
XBLBroadcast bool
|
||||
XBLBroadcastIntent int32
|
||||
XBLBroadcastMode int32
|
||||
Abilities struct {
|
||||
AttackMobs bool `nbt:"attackmobs"`
|
||||
AttackPlayers bool `nbt:"attackplayers"`
|
||||
Build bool `nbt:"build"`
|
||||
Mine bool `nbt:"mine"`
|
||||
DoorsAndSwitches bool `nbt:"doorsandswitches"`
|
||||
FlySpeed float32 `nbt:"flySpeed"`
|
||||
Flying bool `nbt:"flying"`
|
||||
InstantBuild bool `nbt:"instabuild"`
|
||||
Invulnerable bool `nbt:"invulnerable"`
|
||||
Lightning bool `nbt:"lightning"`
|
||||
MayFly bool `nbt:"mayfly"`
|
||||
OP bool `nbt:"op"`
|
||||
OpenContainers bool `nbt:"opencontainers"`
|
||||
PermissionsLevel int32 `nbt:"permissionsLevel"`
|
||||
PlayerPermissionsLevel int32 `nbt:"playerPermissionsLevel"`
|
||||
Teleport bool `nbt:"teleport"`
|
||||
WalkSpeed float32 `nbt:"walkSpeed"`
|
||||
VerticalFlySpeed float32 `nbt:"verticalFlySpeed"`
|
||||
} `nbt:"abilities"`
|
||||
BonusChestEnabled bool `nbt:"bonusChestEnabled"`
|
||||
BonusChestSpawned bool `nbt:"bonusChestSpawned"`
|
||||
CommandBlockOutput bool `nbt:"commandblockoutput"`
|
||||
CommandBlocksEnabled bool `nbt:"commandblocksenabled"`
|
||||
CommandsEnabled bool `nbt:"commandsEnabled"`
|
||||
CurrentTick int64 `nbt:"currentTick"`
|
||||
DoDayLightCycle bool `nbt:"dodaylightcycle"`
|
||||
DoEntityDrops bool `nbt:"doentitydrops"`
|
||||
DoFireTick bool `nbt:"dofiretick"`
|
||||
DoImmediateRespawn bool `nbt:"doimmediaterespawn"`
|
||||
DoInsomnia bool `nbt:"doinsomnia"`
|
||||
DoMobLoot bool `nbt:"domobloot"`
|
||||
DoMobSpawning bool `nbt:"domobspawning"`
|
||||
DoTileDrops bool `nbt:"dotiledrops"`
|
||||
DoWeatherCycle bool `nbt:"doweathercycle"`
|
||||
DrowningDamage bool `nbt:"drowningdamage"`
|
||||
EduLevel bool `nbt:"eduLevel"`
|
||||
EducationFeaturesEnabled bool `nbt:"educationFeaturesEnabled"`
|
||||
ExperimentalGamePlay bool `nbt:"experimentalgameplay"`
|
||||
FallDamage bool `nbt:"falldamage"`
|
||||
FireDamage bool `nbt:"firedamage"`
|
||||
FunctionCommandLimit int32 `nbt:"functioncommandlimit"`
|
||||
HasBeenLoadedInCreative bool `nbt:"hasBeenLoadedInCreative"`
|
||||
HasLockedBehaviourPack bool `nbt:"hasLockedBehaviorPack"`
|
||||
HasLockedResourcePack bool `nbt:"hasLockedResourcePack"`
|
||||
ImmutableWorld bool `nbt:"immutableWorld"`
|
||||
IsCreatedInEditor bool `nbt:"isCreatedInEditor"`
|
||||
IsExportedFromEditor bool `nbt:"isExportedFromEditor"`
|
||||
IsFromLockedTemplate bool `nbt:"isFromLockedTemplate"`
|
||||
IsFromWorldTemplate bool `nbt:"isFromWorldTemplate"`
|
||||
IsWorldTemplateOptionLocked bool `nbt:"isWorldTemplateOptionLocked"`
|
||||
KeepInventory bool `nbt:"keepinventory"`
|
||||
LastOpenedWithVersion []int32 `nbt:"lastOpenedWithVersion"`
|
||||
LightningLevel float32 `nbt:"lightningLevel"`
|
||||
LightningTime int32 `nbt:"lightningTime"`
|
||||
MaxCommandChainLength int32 `nbt:"maxcommandchainlength"`
|
||||
MobGriefing bool `nbt:"mobgriefing"`
|
||||
NaturalRegeneration bool `nbt:"naturalregeneration"`
|
||||
PRID string `nbt:"prid"`
|
||||
PVP bool `nbt:"pvp"`
|
||||
RainLevel float32 `nbt:"rainLevel"`
|
||||
RainTime int32 `nbt:"rainTime"`
|
||||
RandomTickSpeed int32 `nbt:"randomtickspeed"`
|
||||
RequiresCopiedPackRemovalCheck bool `nbt:"requiresCopiedPackRemovalCheck"`
|
||||
SendCommandFeedback bool `nbt:"sendcommandfeedback"`
|
||||
ServerChunkTickRange int32 `nbt:"serverChunkTickRange"`
|
||||
ShowCoordinates bool `nbt:"showcoordinates"`
|
||||
ShowDeathMessages bool `nbt:"showdeathmessages"`
|
||||
SpawnMobs bool `nbt:"spawnMobs"`
|
||||
SpawnRadius int32 `nbt:"spawnradius"`
|
||||
StartWithMapEnabled bool `nbt:"startWithMapEnabled"`
|
||||
TexturePacksRequired bool `nbt:"texturePacksRequired"`
|
||||
TNTExplodes bool `nbt:"tntexplodes"`
|
||||
UseMSAGamerTagsOnly bool `nbt:"useMsaGamertagsOnly"`
|
||||
WorldStartCount int64 `nbt:"worldStartCount"`
|
||||
Experiments map[string]any `nbt:"experiments"`
|
||||
FreezeDamage bool `nbt:"freezedamage"`
|
||||
WorldPolicies map[string]any `nbt:"world_policies"`
|
||||
WorldVersion int32 `nbt:"WorldVersion"`
|
||||
RespawnBlocksExplode bool `nbt:"respawnblocksexplode"`
|
||||
ShowBorderEffect bool `nbt:"showbordereffect"`
|
||||
PermissionsLevel int32 `nbt:"permissionsLevel"`
|
||||
PlayerPermissionsLevel int32 `nbt:"playerPermissionsLevel"`
|
||||
IsRandomSeedAllowed bool `nbt:"isRandomSeedAllowed"`
|
||||
DoLimitedCrafting bool `nbt:"dolimitedcrafting"`
|
||||
EditorWorldType int32 `nbt:"editorWorldType"`
|
||||
PlayersSleepingPercentage int32 `nbt:"playerssleepingpercentage"`
|
||||
RecipesUnlock bool `nbt:"recipesunlock"`
|
||||
NaturalGeneration bool `nbt:"naturalgeneration"`
|
||||
ProjectilesCanBreakBlocks bool `nbt:"projectilescanbreakblocks"`
|
||||
ShowRecipeMessages bool `nbt:"showrecipemessages"`
|
||||
IsHardcore bool `nbt:"IsHardcore"`
|
||||
ShowDaysPlayed bool `nbt:"showdaysplayed"`
|
||||
TNTExplosionDropDecay bool `nbt:"tntexplosiondropdecay"`
|
||||
HasUncompleteWorldFileOnDisk bool `nbt:"HasUncompleteWorldFileOnDisk"`
|
||||
PlayerHasDied bool `nbt:"PlayerHasDied"`
|
||||
UseAllowList bool `nbt:"UseAllowList"`
|
||||
AllowAnonymousBlockDropsInEditorWorlds bool `nbt:"allowAnonymousBlockDropsInEditorWorlds"`
|
||||
PlayerWaypoints int32 `nbt:"playerwaypoints"`
|
||||
ServerEditorConnectionPolicy int32 `nbt:"serverEditorConnectionPolicy"`
|
||||
}
|
||||
|
||||
// FillDefault fills out d with all the default level.dat values.
|
||||
func (d *Data) FillDefault() {
|
||||
d.Abilities.AttackMobs = true
|
||||
d.Abilities.AttackPlayers = true
|
||||
d.Abilities.Build = true
|
||||
d.Abilities.DoorsAndSwitches = true
|
||||
d.Abilities.FlySpeed = 0.05
|
||||
d.Abilities.Mine = true
|
||||
d.Abilities.OpenContainers = true
|
||||
d.Abilities.PlayerPermissionsLevel = 1
|
||||
d.Abilities.WalkSpeed = 0.1
|
||||
d.Abilities.VerticalFlySpeed = 1.0
|
||||
d.BaseGameVersion = "*"
|
||||
d.CommandBlockOutput = true
|
||||
d.CommandBlocksEnabled = true
|
||||
d.CommandsEnabled = true
|
||||
d.Difficulty = 2
|
||||
d.DoDayLightCycle = true
|
||||
d.DoEntityDrops = true
|
||||
d.DoFireTick = true
|
||||
d.DoInsomnia = true
|
||||
d.DoMobLoot = true
|
||||
d.DoMobSpawning = true
|
||||
d.DoTileDrops = true
|
||||
d.DoWeatherCycle = true
|
||||
d.DrowningDamage = true
|
||||
d.FallDamage = true
|
||||
d.FireDamage = true
|
||||
d.FreezeDamage = true
|
||||
d.FunctionCommandLimit = 10000
|
||||
d.GameType = 1
|
||||
d.Generator = 2
|
||||
d.HasBeenLoadedInCreative = true
|
||||
d.InventoryVersion = protocol.CurrentVersion
|
||||
d.LANBroadcast = true
|
||||
d.LANBroadcastIntent = true
|
||||
d.LastOpenedWithVersion = minimumCompatibleClientVersion
|
||||
d.LevelName = "World"
|
||||
d.LightningLevel = 1.0
|
||||
d.LimitedWorldDepth = 16
|
||||
d.LimitedWorldOriginY = math.MaxInt16
|
||||
d.LimitedWorldWidth = 16
|
||||
d.MaxCommandChainLength = math.MaxUint16
|
||||
d.MinimumCompatibleClientVersion = minimumCompatibleClientVersion
|
||||
d.MobGriefing = true
|
||||
d.MultiPlayerGame = true
|
||||
d.MultiPlayerGameIntent = true
|
||||
d.NaturalRegeneration = true
|
||||
d.NetherScale = 8
|
||||
d.NetworkVersion = protocol.CurrentProtocol
|
||||
d.PVP = true
|
||||
d.Platform = 2
|
||||
d.PlatformBroadcastIntent = 3
|
||||
d.RainLevel = 1.0
|
||||
d.RandomSeed = time.Now().Unix()
|
||||
d.RandomTickSpeed = 1
|
||||
d.RespawnBlocksExplode = true
|
||||
d.SendCommandFeedback = true
|
||||
d.ServerChunkTickRange = 6
|
||||
d.ShowBorderEffect = true
|
||||
d.ShowDeathMessages = true
|
||||
d.ShowTags = true
|
||||
d.SpawnMobs = true
|
||||
d.SpawnRadius = 5
|
||||
d.SpawnRadius = 5
|
||||
d.SpawnY = math.MaxInt16
|
||||
d.StorageVersion = 9
|
||||
d.TNTExplodes = true
|
||||
d.WorldVersion = 1
|
||||
d.XBLBroadcastIntent = 3
|
||||
}
|
||||
|
||||
// Settings returns a world.Settings value based on the properties stored in d.
|
||||
func (d *Data) Settings() *world.Settings {
|
||||
d.WorldStartCount += 1
|
||||
difficulty, _ := world.DifficultyByID(int(d.Difficulty))
|
||||
mode, _ := world.GameModeByID(int(d.GameType))
|
||||
return &world.Settings{
|
||||
Name: d.LevelName,
|
||||
Spawn: cube.Pos{int(d.SpawnX), int(d.SpawnY), int(d.SpawnZ)},
|
||||
Time: d.Time,
|
||||
TimeCycle: d.DoDayLightCycle,
|
||||
RainTime: int64(d.RainTime),
|
||||
Raining: d.RainLevel > 0,
|
||||
ThunderTime: int64(d.LightningTime),
|
||||
Thundering: d.LightningLevel > 0,
|
||||
WeatherCycle: d.DoWeatherCycle,
|
||||
CurrentTick: d.CurrentTick,
|
||||
DefaultGameMode: mode,
|
||||
Difficulty: difficulty,
|
||||
TickRange: d.ServerChunkTickRange,
|
||||
}
|
||||
}
|
||||
|
||||
// PutSettings updates d with the Settings stored in s.
|
||||
func (d *Data) PutSettings(s *world.Settings) {
|
||||
d.LevelName = s.Name
|
||||
d.SpawnX, d.SpawnY, d.SpawnZ = int32(s.Spawn.X()), int32(s.Spawn.Y()), int32(s.Spawn.Z())
|
||||
d.LimitedWorldOriginX, d.LimitedWorldOriginY, d.LimitedWorldOriginZ = d.SpawnX, d.SpawnY, d.SpawnZ
|
||||
d.Time = s.Time
|
||||
d.DoDayLightCycle = s.TimeCycle
|
||||
d.DoWeatherCycle = s.WeatherCycle
|
||||
d.RainTime, d.RainLevel = int32(s.RainTime), 0
|
||||
d.LightningTime, d.LightningLevel = int32(s.ThunderTime), 0
|
||||
if s.Raining {
|
||||
d.RainLevel = 1
|
||||
}
|
||||
if s.Thundering {
|
||||
d.LightningLevel = 1
|
||||
}
|
||||
d.CurrentTick = s.CurrentTick
|
||||
d.ServerChunkTickRange = s.TickRange
|
||||
mode, _ := world.GameModeID(s.DefaultGameMode)
|
||||
d.GameType = int32(mode)
|
||||
difficulty, _ := world.DifficultyID(s.Difficulty)
|
||||
d.Difficulty = int32(difficulty)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package leveldat
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/sandertv/gophertunnel/minecraft/nbt"
|
||||
)
|
||||
|
||||
// LevelDat implements the encoding and decoding of level.dat files. An empty
|
||||
// LevelDat is a valid value and may be used to Marshal and Write to a writer or
|
||||
// file afterward.
|
||||
type LevelDat struct {
|
||||
hdr header
|
||||
data []byte
|
||||
}
|
||||
|
||||
// header holds the header for a level.dat file.
|
||||
type header struct {
|
||||
StorageVersion int32
|
||||
FileLength int32
|
||||
}
|
||||
|
||||
// ReadFile reads a level.dat at a path and returns it.
|
||||
func ReadFile(name string) (*LevelDat, error) {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("level.dat: open file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
return Read(bufio.NewReader(f))
|
||||
}
|
||||
|
||||
// Read reads a level.dat from r and returns it.
|
||||
func Read(r io.Reader) (*LevelDat, error) {
|
||||
var ldat LevelDat
|
||||
if err := binary.Read(r, binary.LittleEndian, &ldat.hdr); err != nil {
|
||||
return nil, fmt.Errorf("level.dat: read header: %w", err)
|
||||
}
|
||||
ldat.data = make([]byte, ldat.hdr.FileLength)
|
||||
if n, err := io.ReadFull(r, ldat.data); err != nil || int32(n) != ldat.hdr.FileLength {
|
||||
return nil, fmt.Errorf("level.dat: read data: %w", err)
|
||||
}
|
||||
return &ldat, nil
|
||||
}
|
||||
|
||||
// Unmarshal decodes the level.dat properties from ld into dst. Unmarshal
|
||||
// returns an error if dst was unable to store all properties found in the
|
||||
// level.dat.
|
||||
func (ld *LevelDat) Unmarshal(dst any) error {
|
||||
if err := nbt.UnmarshalEncoding(ld.data, dst, nbt.LittleEndian); err != nil {
|
||||
return fmt.Errorf("level.dat: decode nbt: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ver returns the version of the level.dat decoded, or 0 if ld is the empty
|
||||
// value.
|
||||
func (ld *LevelDat) Ver() int {
|
||||
return int(ld.hdr.StorageVersion)
|
||||
}
|
||||
|
||||
// Marshal encodes src and stores it in the level.dat. src should be either a
|
||||
// struct or a map of fields. Marshal updates the storage version to the latest.
|
||||
func (ld *LevelDat) Marshal(src any) error {
|
||||
var err error
|
||||
ld.data, err = nbt.MarshalEncoding(src, nbt.LittleEndian)
|
||||
if err != nil {
|
||||
return fmt.Errorf("level.dat: encode nbt: %w", err)
|
||||
}
|
||||
ld.hdr = header{
|
||||
StorageVersion: Version,
|
||||
FileLength: int32(len(ld.data)),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes ld to w.
|
||||
func (ld *LevelDat) Write(w io.Writer) error {
|
||||
if err := binary.Write(w, binary.LittleEndian, ld.hdr); err != nil {
|
||||
return fmt.Errorf("level.dat: write header: %w", err)
|
||||
}
|
||||
if _, err := w.Write(ld.data); err != nil {
|
||||
return fmt.Errorf("level.dat: write data: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteFile writes ld to a file at name.
|
||||
func (ld *LevelDat) WriteFile(name string) error {
|
||||
f, err := os.OpenFile(name, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("level.dat: open file: %w", err)
|
||||
}
|
||||
w := bufio.NewWriter(f)
|
||||
defer func() {
|
||||
_ = w.Flush()
|
||||
_ = f.Close()
|
||||
}()
|
||||
return ld.Write(w)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package leveldat
|
||||
|
||||
import (
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Version is the current version stored in level.dat files.
|
||||
const Version = 10
|
||||
|
||||
// minimumCompatibleClientVersion is the minimum compatible client version,
|
||||
// required by the latest Minecraft data provider.
|
||||
var minimumCompatibleClientVersion []int32
|
||||
|
||||
// init initialises the minimum compatible client version.
|
||||
func init() {
|
||||
fullVersion := append(strings.Split(protocol.CurrentVersion, "."), "0", "0")
|
||||
for _, v := range fullVersion {
|
||||
i, _ := strconv.Atoi(v)
|
||||
minimumCompatibleClientVersion = append(minimumCompatibleClientVersion, int32(i))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user