diff --git a/server/player/player.go b/server/player/player.go index 9d9439dcf..7de575b0c 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -1827,7 +1827,7 @@ func (p *Player) PickBlock(pos cube.Pos) { if found { if slot < 9 { - _ = p.session().SetHeldSlot(slot) + _ = p.session().SetHeldSlot(slot, p.tx, p) return } _ = p.Inventory().Swap(slot, int(*p.heldSlot)) @@ -1840,7 +1840,7 @@ func (p *Player) PickBlock(pos cube.Pos) { return } if firstEmpty < 8 { - _ = p.session().SetHeldSlot(firstEmpty) + _ = p.session().SetHeldSlot(firstEmpty, p.tx, p) _ = p.Inventory().SetItem(firstEmpty, pickedItem) return } diff --git a/server/player/type.go b/server/player/type.go index 2045a1209..df38c0503 100644 --- a/server/player/type.go +++ b/server/player/type.go @@ -28,14 +28,13 @@ type Config struct { Session *session.Session } -func (t Type) Init(conf any, data *world.EntityData) { - cfg, _ := conf.(Config) - locale, err := language.Parse(strings.Replace(cfg.Locale, "_", "-", 1)) +func (conf Config) Apply(data *world.EntityData) { + locale, err := language.Parse(strings.Replace(conf.Locale, "_", "-", 1)) if err != nil { locale = language.BritishEnglish } - data.Name = cfg.Name - data.Pos = cfg.Pos + data.Name = conf.Name + data.Pos = conf.Pos data.Data = &playerData{ inv: inventory.New(36, nil), enderChest: inventory.New(27, nil), @@ -51,12 +50,12 @@ func (t Type) Init(conf any, data *world.EntityData) { mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true}, heldSlot: new(uint32), gameMode: world.GameModeSurvival, - skin: cfg.Skin, + skin: conf.Skin, airSupplyTicks: 300, maxAirSupplyTicks: 300, enchantSeed: rand.Int63(), scale: 1.0, - s: cfg.Session, + s: conf.Session, } } diff --git a/server/session/handler_container_close.go b/server/session/handler_container_close.go index 9500f3529..9fdd9136a 100644 --- a/server/session/handler_container_close.go +++ b/server/session/handler_container_close.go @@ -13,7 +13,7 @@ type ContainerCloseHandler struct{} func (h *ContainerCloseHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.ContainerClose) - s.EmptyUIInventory() + s.EmptyUIInventory(c) switch pk.WindowID { case 0: // Closing of the normal inventory. diff --git a/server/session/player.go b/server/session/player.go index f3987ff9d..9f94d82e4 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -74,7 +74,7 @@ func (s *Session) closeCurrentContainer(tx *world.Tx) { // EmptyUIInventory attempts to move all items in the UI inventory to the player's main inventory. If the main inventory // is full, the items are dropped on the ground instead. -func (s *Session) EmptyUIInventory() { +func (s *Session) EmptyUIInventory(c Controllable) { if s == Nop { return } @@ -82,15 +82,15 @@ func (s *Session) EmptyUIInventory() { if n, err := s.inv.AddItem(i); err != nil { // We couldn't add the item to the main inventory (probably because // it was full), so we drop it instead. - s.c.Drop(i.Grow(i.Count() - n)) + c.Drop(i.Grow(i.Count() - n)) } } } // SendRespawn spawns the Controllable entity of the session client-side in the world, provided it has died. -func (s *Session) SendRespawn(pos mgl64.Vec3) { +func (s *Session) SendRespawn(pos mgl64.Vec3, c Controllable) { s.writePacket(&packet.Respawn{ - Position: vec64To32(pos.Add(entityOffset(s.c))), + Position: vec64To32(pos.Add(entityOffset(c))), State: packet.RespawnStateReadyToSpawn, EntityRuntimeID: selfEntityRuntimeID, }) @@ -520,7 +520,7 @@ func (s *Session) EnableInstantRespawn(enable bool) { // addToPlayerList adds the player of a session to the player list of this session. It will be shown in the // in-game pause menu screen. -func (s *Session) addToPlayerList(session *Session) { +func (s *Session) addToPlayerList(session *Session, c Controllable) { runtimeID := uint64(1) s.entityMutex.Lock() if session != s { @@ -663,18 +663,18 @@ func (s *Session) broadcastArmourFunc(tx *world.Tx, c Controllable) func(slot in } // SetHeldSlot sets the currently held hotbar slot. -func (s *Session) SetHeldSlot(slot int) error { +func (s *Session) SetHeldSlot(slot int, tx *world.Tx, c Controllable) error { if slot > 8 { return fmt.Errorf("slot exceeds hotbar range 0-8: slot is %v", slot) } - s.heldSlot.Store(uint32(slot)) + *s.heldSlot = uint32(slot) - for _, viewer := range s.c.World().Viewers(s.c.Position()) { - viewer.ViewEntityItems(s.c) + for _, viewer := range tx.Viewers(c.Position()) { + viewer.ViewEntityItems(c) } - mainHand, _ := s.c.HeldItems() + mainHand, _ := c.HeldItems() s.writePacket(&packet.MobEquipment{ EntityRuntimeID: selfEntityRuntimeID, NewItem: instanceFromItem(mainHand), diff --git a/server/world/chunk/col.go b/server/world/chunk/col.go new file mode 100644 index 000000000..6b412556d --- /dev/null +++ b/server/world/chunk/col.go @@ -0,0 +1,21 @@ +package chunk + +import ( + "github.com/df-mc/dragonfly/server/block/cube" +) + +type Column struct { + Chunk *Chunk + Entities []Entity + BlockEntities []BlockEntity +} + +type BlockEntity struct { + Pos cube.Pos + Data map[string]any +} + +type Entity struct { + ID int64 + Data map[string]any +} diff --git a/server/world/entity.go b/server/world/entity.go index 08e74d81a..101a1e157 100644 --- a/server/world/entity.go +++ b/server/world/entity.go @@ -13,7 +13,6 @@ import ( type AdvancedEntityType interface { EntityType - Init(conf any, data *EntityData) From(tx *Tx, handle *EntityHandle, data *EntityData) Entity } @@ -28,9 +27,13 @@ type EntityHandle struct { // HANDLER?? HANDLE WORLD CHANGE HERE } -func NewEntity(t AdvancedEntityType, conf any) *EntityHandle { +type EntityConfig interface { + Apply(data *EntityData) +} + +func NewEntity(t AdvancedEntityType, conf EntityConfig) *EntityHandle { handle := &EntityHandle{id: uuid.New(), t: t} - t.Init(conf, &handle.data) + conf.Apply(&handle.data) return handle } diff --git a/server/world/mcdb/conf.go b/server/world/mcdb/conf.go index 150dec705..22069380b 100644 --- a/server/world/mcdb/conf.go +++ b/server/world/mcdb/conf.go @@ -2,8 +2,6 @@ package mcdb import ( "fmt" - "github.com/df-mc/dragonfly/server/entity" - "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" @@ -17,23 +15,9 @@ 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 - // Compression specifies the compression to use for compressing new data in - // the database. Decompression of the database will happen based on IDs - // found in the compressed blocks and is therefore uninfluenced by this - // field. If left empty, Compression will default to opt.FlateCompression. - Compression opt.Compression - // BlockSize specifies the size of blocks to be compressed. The default - // value, when left empty, is 16KiB (16 * opt.KiB). Higher values generally - // lead to better compression ratios at the expense of slightly higher - // memory usage while (de)compressing. - BlockSize int - // ReadOnly opens the DB in read-only mode. This will leave the data in the - // database unedited. - ReadOnly bool - - // Entities is an EntityRegistry with all entity types registered that may - // be read from the DB. Entities will default to entity.DefaultRegistry. - Entities world.EntityRegistry + // LDBOptions holds LevelDB specific default options, such as the block size + // or compression used in the database. + LDBOptions *opt.Options } // Open creates a new DB reading and writing from/to files under the path @@ -45,11 +29,11 @@ func (conf Config) Open(dir string) (*DB, error) { conf.Log = slog.Default() } conf.Log = conf.Log.With("provider", "mcdb") - if conf.BlockSize == 0 { - conf.BlockSize = 16 * opt.KiB + if conf.LDBOptions == nil { + conf.LDBOptions = new(opt.Options) } - if len(conf.Entities.Types()) == 0 { - conf.Entities = entity.DefaultRegistry + if conf.LDBOptions.BlockSize == 0 { + conf.LDBOptions.BlockSize = 16 * opt.KiB } _ = os.MkdirAll(filepath.Join(dir, "db"), 0777) @@ -62,10 +46,6 @@ func (conf Config) Open(dir string) (*DB, error) { if err != nil { return nil, fmt.Errorf("open db: read level.dat: %w", err) } - - // TODO: Perform proper conversion here. Dragonfly stored 3 for a long - // time even though the fields were up to date, so we have to accept - // older ones no matter what. ver := ldat.Ver() if ver != leveldat.Version && ver >= 10 { return nil, fmt.Errorf("open db: level.dat version %v is unsupported", ver) @@ -75,11 +55,7 @@ func (conf Config) Open(dir string) (*DB, error) { } } db.set = db.ldat.Settings() - ldb, err := leveldb.OpenFile(filepath.Join(dir, "db"), &opt.Options{ - Compression: conf.Compression, - BlockSize: conf.BlockSize, - ReadOnly: conf.ReadOnly, - }) + ldb, err := leveldb.OpenFile(filepath.Join(dir, "db"), conf.LDBOptions) if err != nil { return nil, fmt.Errorf("open db: leveldb: %w", err) } diff --git a/server/world/mcdb/db.go b/server/world/mcdb/db.go index d8f66ec6e..93f03ee71 100644 --- a/server/world/mcdb/db.go +++ b/server/world/mcdb/db.go @@ -12,9 +12,10 @@ import ( "github.com/df-mc/goleveldb/leveldb" "github.com/google/uuid" "github.com/sandertv/gophertunnel/minecraft/nbt" - "golang.org/x/exp/maps" + "math/rand" "os" "path/filepath" + "slices" "time" ) @@ -101,31 +102,24 @@ func (db *DB) SavePlayerSpawnPosition(id uuid.UUID, pos cube.Pos) error { k := "player_server_" + id.String() if errors.Is(err, leveldb.ErrNotFound) { - data, err := nbt.MarshalEncoding(playerData{ - UUID: id.String(), - ServerID: k, - }, nbt.LittleEndian) + 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("error writing player data for id %v: %w", id, err) - } - } else { - if d, k, _, err = db.loadPlayerData(id); err != nil { - return err + 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"] = int32(pos.X()) - d["SpawnY"] = int32(pos.Y()) - d["SpawnZ"] = int32(pos.Z()) + 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("error writing server data for player %v: %w", id, err) + return fmt.Errorf("write server data for player %v: %w", id, err) } return nil } @@ -133,7 +127,7 @@ func (db *DB) SavePlayerSpawnPosition(id uuid.UUID, pos cube.Pos) error { // 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) (*world.Column, error) { +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 { @@ -144,9 +138,9 @@ func (db *DB) LoadColumn(pos world.ChunkPos, dim world.Dimension) (*world.Column const chunkVersion = 40 -func (db *DB) column(k dbKey) (*world.Column, error) { +func (db *DB) column(k dbKey) (*chunk.Column, error) { var cdata chunk.SerialisedData - col := new(world.Column) + col := new(chunk.Column) ver, err := db.version(k) if err != nil { @@ -174,7 +168,7 @@ func (db *DB) column(k dbKey) (*world.Column, error) { // 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, col.Chunk) + 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) @@ -184,22 +178,20 @@ func (db *DB) column(k dbKey) (*world.Column, error) { func (db *DB) version(k dbKey) (byte, error) { p, err := db.ldb.Get(k.Sum(keyVersion), nil) - switch err { - default: - return 0, err - case leveldb.ErrNotFound: + 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 } - fallthrough - case nil: - if n := len(p); n != 1 { - return 0, fmt.Errorf("expected 1 version byte, found %v", n) - } - return p[0], nil } + 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) { @@ -208,7 +200,7 @@ func (db *DB) biomes(k dbKey) ([]byte, error) { 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 is discarded. + // 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) } @@ -223,7 +215,7 @@ func (db *DB) subChunks(k dbKey) ([][]byte, error) { for i := range sub { y := uint8(i + (r[0] >> 4)) sub[i], err = db.ldb.Get(k.Sum(keySubChunkData, y), nil) - if err == leveldb.ErrNotFound { + 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 @@ -234,44 +226,57 @@ func (db *DB) subChunks(k dbKey) ([][]byte, error) { return sub, nil } -func (db *DB) entities(k dbKey) ([]world.Entity, error) { - data, err := db.ldb.Get(k.Sum(keyEntities), 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 []world.Entity + var entities []chunk.Entity buf := bytes.NewBuffer(data) - dec := nbt.NewDecoderWithEncoding(buf, nbt.LittleEndian) + dec, ok := nbt.NewDecoderWithEncoding(buf, nbt.LittleEndian), false - var m map[string]any for buf.Len() != 0 { - maps.Clear(m) - if err := dec.Decode(&m); err != nil { - return nil, fmt.Errorf("decode nbt: %w", err) - } - id, ok := m["identifier"] - if !ok { - db.conf.Log.Error("missing identifier field", "data", fmt.Sprint(m)) - continue + 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) } - name, _ := id.(string) - t, ok := db.conf.Entities.Lookup(name) + ent.ID, ok = ent.Data["UniqueID"].(int64) if !ok { - db.conf.Log.Error("no entity with ID", "id", name, "data", fmt.Sprint(m)) - continue - } - if s, ok := t.(world.SaveableEntityType); ok { - if v := s.DecodeNBT(m); v != nil { - entities = append(entities, v) - } + db.conf.Log.Error("missing unique ID field, generating random", "data", fmt.Sprint(ent.Data)) + ent.ID = rand.Int63() } + entities = append(entities, ent) } return entities, nil } -func (db *DB) blockEntities(k dbKey, c *chunk.Chunk) (map[cube.Pos]world.Block, error) { - blockEntities := make(map[cube.Pos]world.Block) +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 { @@ -281,33 +286,20 @@ func (db *DB) blockEntities(k dbKey, c *chunk.Chunk) (map[cube.Pos]world.Block, buf := bytes.NewBuffer(data) dec := nbt.NewDecoderWithEncoding(buf, nbt.LittleEndian) - var m map[string]any for buf.Len() != 0 { - maps.Clear(m) - if err := dec.Decode(&m); err != nil { + be := chunk.BlockEntity{Data: make(map[string]any)} + if err := dec.Decode(&be.Data); err != nil { return blockEntities, fmt.Errorf("decode nbt: %w", err) } - pos := blockPosFromNBT(m) - - id := c.Block(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0) - b, ok := world.BlockByRuntimeID(id) - if !ok { - db.conf.Log.Error("no block with runtime ID", "ID", id) - continue - } - nbter, ok := b.(world.NBTer) - if !ok { - db.conf.Log.Error("block with nbt does not implement world.NBTer", "block", fmt.Sprintf("%#v", b)) - continue - } - blockEntities[pos] = nbter.DecodeNBT(m).(world.Block) + be.Pos = blockPosFromNBT(be.Data) + blockEntities = append(blockEntities, be) } return blockEntities, 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 *world.Column) error { +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) @@ -315,9 +307,9 @@ func (db *DB) StoreColumn(pos world.ChunkPos, dim world.Dimension, col *world.Co return nil } -func (db *DB) storeColumn(k dbKey, col *world.Column) error { +func (db *DB) storeColumn(k dbKey, col *chunk.Column) error { data := chunk.Encode(col.Chunk, chunk.DiskEncoding) - n := 5 + len(data.SubChunks) + n := 6 + len(data.SubChunks) + len(col.Entities) batch := leveldb.MakeBatch(n) db.storeVersion(batch, k, chunkVersion) @@ -352,29 +344,59 @@ func (db *DB) storeFinalisation(batch *leveldb.Batch, k dbKey, finalisation uint batch.Put(k.Sum(keyFinalisation), p) } -func (db *DB) storeEntities(batch *leveldb.Batch, k dbKey, entities []world.Entity) { - if len(entities) == 0 { - batch.Delete(k.Sum(keyEntities)) - return +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:]))) + } } - buf := bytes.NewBuffer(nil) - enc := nbt.NewEncoderWithEncoding(buf, nbt.LittleEndian) + newIDs := make([]int64, 0, len(entities)) for _, e := range entities { - t, ok := e.Type().(world.SaveableEntityType) - if !ok { + 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 } - x := t.EncodeNBT(e) - x["identifier"] = t.EncodeEntity() - if err := enc.Encode(x); err != nil { - db.conf.Log.Error("store entities: encode NBT: " + err.Error()) + 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) } - batch.Put(k.Sum(keyEntities), buf.Bytes()) + + // Remove old entity data for this chunk. + batch.Delete(append(index(k.pos, k.dim), keyEntitiesOld)) +} + +func entityIndex(id int64) []byte { + return binary.LittleEndian.AppendUint64([]byte(keyEntity), uint64(id)) } -func (db *DB) storeBlockEntities(batch *leveldb.Batch, k dbKey, blockEntities map[cube.Pos]world.Block) { +func (db *DB) storeBlockEntities(batch *leveldb.Batch, k dbKey, blockEntities []chunk.BlockEntity) { if len(blockEntities) == 0 { batch.Delete(k.Sum(keyBlockEntities)) return @@ -382,14 +404,9 @@ func (db *DB) storeBlockEntities(batch *leveldb.Batch, k dbKey, blockEntities ma buf := bytes.NewBuffer(nil) enc := nbt.NewEncoderWithEncoding(buf, nbt.LittleEndian) - for pos, b := range blockEntities { - n, ok := b.(world.NBTer) - if !ok { - continue - } - data := n.EncodeNBT() - data["x"], data["y"], data["z"] = int32(pos[0]), int32(pos[1]), int32(pos[2]) - if err := enc.Encode(data); err != nil { + 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()) } } diff --git a/server/world/mcdb/iterator.go b/server/world/mcdb/iterator.go index a9d0c35f6..d5750974d 100644 --- a/server/world/mcdb/iterator.go +++ b/server/world/mcdb/iterator.go @@ -4,6 +4,7 @@ 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" ) @@ -25,7 +26,7 @@ type ColumnIterator struct { err error - current *world.Column + current *chunk.Column pos world.ChunkPos dim world.Dimension seen map[dbKey]struct{} @@ -84,7 +85,7 @@ func (iter *ColumnIterator) Next() bool { } // Column returns the value of the current position/column pair, or nil if none. -func (iter *ColumnIterator) Column() *world.Column { +func (iter *ColumnIterator) Column() *chunk.Column { return iter.current } diff --git a/server/world/mcdb/keys.go b/server/world/mcdb/keys.go index 8e059c896..ac21ba4dd 100644 --- a/server/world/mcdb/keys.go +++ b/server/world/mcdb/keys.go @@ -17,9 +17,9 @@ const ( // 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 - // keyEntities holds n amount of NBT compound tags appended to each other (not a TAG_List, just appended). The + // 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. - keyEntities = '2' // 32 + keyEntitiesOld = '2' // 32 // 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. @@ -32,6 +32,10 @@ const ( // 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. diff --git a/server/world/provider.go b/server/world/provider.go index 5ff381598..150bfa4ee 100644 --- a/server/world/provider.go +++ b/server/world/provider.go @@ -2,6 +2,7 @@ package world import ( "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world/chunk" "github.com/df-mc/goleveldb/leveldb" "github.com/google/uuid" "io" @@ -24,10 +25,10 @@ type Provider interface { // 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. - LoadColumn(pos ChunkPos, dim Dimension) (*Column, error) + LoadColumn(pos ChunkPos, dim Dimension) (*chunk.Column, error) // StoreColumn stores a world.Column at a position and dimension in the DB. // An error is returned if storing was unsuccessful. - StoreColumn(pos ChunkPos, dim Dimension, col *Column) error + StoreColumn(pos ChunkPos, dim Dimension, col *chunk.Column) error } // Compile time check to make sure NopProvider implements Provider. @@ -47,9 +48,11 @@ func (n NopProvider) Settings() *Settings { } return n.Set } -func (NopProvider) SaveSettings(*Settings) {} -func (NopProvider) LoadColumn(ChunkPos, Dimension) (*Column, error) { return nil, leveldb.ErrNotFound } -func (NopProvider) StoreColumn(ChunkPos, Dimension, *Column) error { return nil } +func (NopProvider) SaveSettings(*Settings) {} +func (NopProvider) LoadColumn(ChunkPos, Dimension) (*chunk.Column, error) { + return nil, leveldb.ErrNotFound +} +func (NopProvider) StoreColumn(ChunkPos, Dimension, *chunk.Column) error { return nil } func (NopProvider) LoadPlayerSpawnPosition(uuid.UUID) (cube.Pos, bool, error) { return cube.Pos{}, false, nil } diff --git a/server/world/world.go b/server/world/world.go index 66a9923e4..b13311ae4 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -1,7 +1,9 @@ package world import ( + "encoding/binary" "errors" + "fmt" "math/rand" "sync" "time" @@ -124,7 +126,7 @@ func (w *World) handleTransactions() { case queuedTx := <-w.queue: tx := &Tx{w: w} queuedTx.f(tx) - queuedTx.c <- struct{}{} + close(queuedTx.c) case <-w.closing: w.running.Done() return @@ -403,8 +405,8 @@ func (w *World) buildStructure(pos cube.Pos, s Structure) { c.SetBlock(0, 0, 0, 0, c.Block(0, 0, 0, 0)) // Make sure the heightmap is recalculated. c.modified = true - // After setting all blocks of the structure within a single chunk, we show the new chunk to all - // viewers once, and unlock it. + // After setting all blocks of the structure within a single chunk, + // we show the new chunk to all viewers once. for _, viewer := range c.viewers { viewer.ViewChunk(chunkPos, c.Chunk, c.BlockEntities) } @@ -545,26 +547,27 @@ func (w *World) additionalLiquid(pos cube.Pos) (Liquid, bool) { // The light value returned is a value in the range 0-15, where 0 means there is no light present, whereas // 15 means the block is fully lit. func (w *World) light(pos cube.Pos) uint8 { - if pos[1] < w.Range()[0] { + if pos[1] < w.ra[0] { // Fast way out. return 0 } - if pos[1] > w.Range()[1] { + if pos[1] > w.ra[1] { // Above the rest of the world, so full skylight. return 15 } return w.chunk(chunkPosFromBlockPos(pos)).Light(uint8(pos[0]), int16(pos[1]), uint8(pos[2])) } -// SkyLight returns the skylight level at the position passed. This light level is not influenced by blocks -// that emit light, such as torches or glowstone. The light value, similarly to Light, is a value in the -// range 0-15, where 0 means no light is present. +// SkyLight returns the skylight level at the position passed. This light level +// is not influenced by blocks that emit light, such as torches. The light +// value, similarly to Light, is a value in the range 0-15, where 0 means no +// light is present. func (w *World) skyLight(pos cube.Pos) uint8 { - if w == nil || pos[1] < w.Range()[0] { + if pos[1] < w.ra[0] { // Fast way out. return 0 } - if pos[1] > w.Range()[1] { + if pos[1] > w.ra[1] { // Above the rest of the world, so full skylight. return 15 } @@ -629,19 +632,13 @@ func (w *World) temperature(pos cube.Pos) float64 { tempDrop = 1.0 / 600 seaLevel = 64 ) - diff := pos[1] - seaLevel - if diff < 0 { - diff = 0 - } + diff := max(pos[1]-seaLevel, 0) return w.biome(pos).Temperature() - float64(diff)*tempDrop } // AddParticle spawns a particle at a given position in the world. Viewers that are viewing the chunk will be // shown the particle. func (w *World) addParticle(pos mgl64.Vec3, p Particle) { - if w == nil { - return - } p.Spawn(w, pos) for _, viewer := range w.viewersOf(pos) { viewer.ViewParticle(pos, p) @@ -1097,25 +1094,10 @@ func (w *World) chunk(pos ChunkPos) *Column { return c } -// setChunk sets the chunk.Chunk passed at a specific ChunkPos without replacing any entities at that -// position. -// -//lint:ignore U1000 This method is explicitly present to be used using compiler directives. -func (w *World) setChunk(pos ChunkPos, c *chunk.Chunk, e map[cube.Pos]Block) { - if w == nil { - return - } - col := newColumn(c) - maps.Copy(col.BlockEntities, e) - if o, ok := w.chunks[pos]; ok { - col.viewers = o.viewers - } - w.chunks[pos] = col -} - // loadChunk attempts to load a chunk from the provider, or generates a chunk if one doesn't currently exist. func (w *World) loadChunk(pos ChunkPos) (*Column, error) { - col, err := w.provider().LoadColumn(pos, w.conf.Dim) + column, err := w.provider().LoadColumn(pos, w.conf.Dim) + col := columnFrom(column, w) switch { case err == nil: w.chunks[pos] = col @@ -1136,16 +1118,18 @@ func (w *World) loadChunk(pos ChunkPos) (*Column, error) { } } -// calculateLight calculates the light in the chunk passed and spreads the light of any of the surrounding -// neighbours if they have all chunks loaded around it as a result of the one passed. +// calculateLight calculates the light in the chunk passed and spreads the +// light of any surrounding neighbours if they have all chunks loaded around it +// as a result of the one passed. func (w *World) calculateLight(centre ChunkPos) { for x := int32(-1); x <= 1; x++ { for z := int32(-1); z <= 1; z++ { - // For all the neighbours of this chunk, if they exist, check if all neighbours of that chunk - // now exist because of this one. + // For all the neighbours of this chunk, if they exist, check if all + // neighbours of that chunk now exist because of this one. pos := ChunkPos{centre[0] + x, centre[1] + z} if _, ok := w.chunks[pos]; ok { - // Attempt to spread the light of all neighbours into the ones surrounding them. + // Attempt to spread the light of all neighbours into the + // surrounding ones. w.spreadLight(pos) } } @@ -1176,7 +1160,7 @@ func (w *World) spreadLight(pos ChunkPos) { func (w *World) saveChunk(tx *Tx, pos ChunkPos, c *Column, closeEntities bool) { if !w.conf.ReadOnly && c.modified { c.Compact() - if err := w.provider().StoreColumn(pos, w.conf.Dim, c); err != nil { + if err := w.provider().StoreColumn(pos, w.conf.Dim, columnTo(c, tx)); err != nil { w.conf.Log.Error("save chunk: "+err.Error(), "X", pos[0], "Z", pos[1]) } } @@ -1231,3 +1215,61 @@ type Column struct { func newColumn(c *chunk.Chunk) *Column { return &Column{Chunk: c, BlockEntities: map[cube.Pos]Block{}} } + +func columnTo(col *Column, tx *Tx) *chunk.Column { + c := &chunk.Column{ + Chunk: col.Chunk, + Entities: make([]chunk.Entity, 0, len(col.Entities)), + BlockEntities: make([]chunk.BlockEntity, 0, len(col.BlockEntities)), + } + for _, e := range col.Entities { + st := e.t.(SaveableEntityType) + data := st.EncodeNBT(e.Entity(tx)) + data["identifier"] = st.EncodeEntity() + c.Entities = append(c.Entities, chunk.Entity{ID: int64(binary.LittleEndian.Uint64(e.id[8:])), Data: data}) + } + for pos, be := range col.BlockEntities { + c.BlockEntities = append(c.BlockEntities, chunk.BlockEntity{Pos: pos, Data: be.(NBTer).EncodeNBT()}) + } + return c +} + +func columnFrom(c *chunk.Column, w *World) *Column { + col := &Column{ + Chunk: c.Chunk, + Entities: make([]*EntityHandle, 0, len(c.Entities)), + BlockEntities: make(map[cube.Pos]Block, len(c.BlockEntities)), + } + for _, e := range c.Entities { + eid, ok := e.Data["identifier"].(string) + if !ok { + w.conf.Log.Error("read column: entity without identifier field", "ID", e.ID) + continue + } + t, ok := w.conf.Entities.Lookup(eid) + if !ok { + w.conf.Log.Error("read column: unknown entity type", "ID", e.ID, "type", eid) + continue + } + ent := NewEntity(t, t.(SaveableEntityType).DecodeNBT(e.Data)) // TODO: Figure out what to do with this. + ent.id = uuid.UUID{} + binary.LittleEndian.PutUint64(ent.id[8:], uint64(e.ID)) + col.Entities = append(col.Entities, ent) + } + for _, be := range c.BlockEntities { + rid := c.Chunk.Block(uint8(be.Pos[0]), int16(be.Pos[1]), uint8(be.Pos[2]), 0) + b, ok := BlockByRuntimeID(rid) + if !ok { + w.conf.Log.Error("read column: no block with runtime ID", "ID", rid) + continue + } + nb, ok := b.(NBTer) + if !ok { + w.conf.Log.Error("read column: block with nbt does not implement NBTer", "block", fmt.Sprintf("%#v", b)) + continue + } + col.BlockEntities[be.Pos] = nb.DecodeNBT(be.Data).(Block) + } + + return col +}